diff --git a/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx b/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx index 16afba068..92a3c820b 100644 --- a/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx @@ -7,6 +7,7 @@ import { useOutletContext } from 'react-router-dom'; import DetailsForm from '@/components/DetailsForm'; import FormCard from '@/components/FormCard'; import FormField from '@/components/FormField'; +import Switch from '@/components/Switch'; import TextInput from '@/components/TextInput'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import useApi from '@/hooks/use-api'; @@ -32,14 +33,20 @@ function ApiResourceSettings() { const api = useApi(); - const onSubmit = handleSubmit(async (formData) => { + const onSubmit = handleSubmit(async ({ isDefault, ...rest }) => { if (isSubmitting) { return; } - const updatedApiResource = await api - .patch(`api/resources/${resource.id}`, { json: formData }) - .json(); + const [data] = await Promise.all([ + api.patch(`api/resources/${resource.id}`, { json: rest }).json(), + api + .patch(`api/resources/${resource.id}/is-default`, { json: { isDefault } }) + .json(), + ]); + + // We cannot ensure the order of API requests, manually combine the results + const updatedApiResource = { ...data, isDefault }; reset(updatedApiResource); onResourceUpdated(updatedApiResource); toast.success(t('general.saved')); @@ -77,6 +84,11 @@ function ApiResourceSettings() { placeholder={t('api_resource_details.token_expiration_time_in_seconds_placeholder')} /> + {!isLogtoManagementApiResource && ( + + + + )} diff --git a/packages/core/src/errors/SlonikError/index.ts b/packages/core/src/errors/SlonikError/index.ts index 82a3c9aac..d74cf1365 100644 --- a/packages/core/src/errors/SlonikError/index.ts +++ b/packages/core/src/errors/SlonikError/index.ts @@ -3,14 +3,8 @@ import type { OmitAutoSetFields, UpdateWhereData } from '@logto/shared'; import { SlonikError } from 'slonik'; export class DeletionError extends SlonikError { - table?: string; - id?: string; - - public constructor(table?: string, id?: string) { + public constructor(public readonly table?: string, public readonly id?: string) { super('Resource not found.'); - - this.table = table; - this.id = id; } } @@ -18,17 +12,11 @@ export class UpdateError< CreateSchema extends SchemaLike, Schema extends CreateSchema > extends SlonikError { - schema: GeneratedSchema; - detail: UpdateWhereData; - public constructor( - schema: GeneratedSchema, - detail: UpdateWhereData + public readonly schema: GeneratedSchema, + public readonly detail: Partial> ) { super('Resource not found.'); - - this.schema = schema; - this.detail = detail; } } @@ -36,16 +24,10 @@ export class InsertionError< CreateSchema extends SchemaLike, Schema extends CreateSchema > extends SlonikError { - schema: GeneratedSchema; - detail?: OmitAutoSetFields; - public constructor( - schema: GeneratedSchema, - detail?: OmitAutoSetFields + public readonly schema: GeneratedSchema, + public readonly detail?: OmitAutoSetFields ) { super('Create Error.'); - - this.schema = schema; - this.detail = detail; } } diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 06620e63a..056c813c9 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -48,7 +48,7 @@ export default function initOidc( defaultRefreshTokenTtl, } = envSet.oidc; const { - resources: { findResourceByIndicator }, + resources: { findResourceByIndicator, findDefaultResource }, users: { findUserById }, } = queries; const { findUserScopesForResourceIndicator } = libraries.users; @@ -105,7 +105,10 @@ export default function initOidc( // https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#featuresresourceindicators resourceIndicators: { enabled: true, - defaultResource: () => '', + defaultResource: async () => { + const resource = await findDefaultResource(); + return resource?.indicator ?? ''; + }, // Disable the auto use of authorization_code granted resource feature useGrantedResource: () => false, getResourceServerInfo: async (ctx, indicator) => { diff --git a/packages/core/src/queries/resource.ts b/packages/core/src/queries/resource.ts index c1db5ed57..1fe69be4b 100644 --- a/packages/core/src/queries/resource.ts +++ b/packages/core/src/queries/resource.ts @@ -10,7 +10,7 @@ import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js' import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; import { getTotalRowCountWithPool } from '#src/database/row-count.js'; import { buildUpdateWhereWithPool } from '#src/database/update-where.js'; -import { DeletionError } from '#src/errors/SlonikError/index.js'; +import { DeletionError, UpdateError } from '#src/errors/SlonikError/index.js'; const { table, fields } = convertToIdentifiers(Resources); @@ -26,6 +26,35 @@ export const createResourceQueries = (pool: CommonQueryMethods) => { where ${fields.indicator}=${indicator} `); + const findDefaultResource = async () => + pool.maybeOne(sql` + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + where ${fields.isDefault}=true + `); + + const setDefaultResource = async (id: string) => { + return pool.transaction(async (connection) => { + await connection.query(sql` + update ${table} + set ${fields.isDefault}=false + where ${fields.id}!=${id}; + `); + const returning = await connection.maybeOne(sql` + update ${table} + set ${fields.isDefault}=true + where ${fields.id}=${id} + returning *; + `); + + if (!returning) { + throw new UpdateError(Resources, { set: { isDefault: true }, where: { id } }); + } + + return returning; + }); + }; + const findResourceById = buildFindEntityByIdWithPool(pool)(Resources); const findResourcesByIds = async (resourceIds: string[]) => @@ -64,6 +93,8 @@ export const createResourceQueries = (pool: CommonQueryMethods) => { findTotalNumberOfResources, findAllResources, findResourceByIndicator, + findDefaultResource, + setDefaultResource, findResourceById, findResourcesByIds, insertResource, diff --git a/packages/core/src/routes/resource.ts b/packages/core/src/routes/resource.ts index 6db684e61..6f487e71a 100644 --- a/packages/core/src/routes/resource.ts +++ b/packages/core/src/routes/resource.ts @@ -1,7 +1,7 @@ import { Resources, Scopes } from '@logto/schemas'; import { buildIdGenerator } from '@logto/shared'; import { tryThat, yes } from '@silverhand/essentials'; -import { object, string } from 'zod'; +import { boolean, object, string } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; @@ -23,6 +23,7 @@ export default function resourceRoutes( findAllResources, findResourceById, findResourceByIndicator, + setDefaultResource, insertResource, updateResourceById, deleteResourceById, @@ -76,7 +77,9 @@ export default function resourceRoutes( router.post( '/resources', koaGuard({ - body: Resources.createGuard.omit({ id: true }), + // Intentionally omit `isDefault` since it'll affect other rows. + // Use the dedicated API `PATCH /resources/:id/is-default` to update. + body: Resources.createGuard.omit({ id: true, isDefault: true }), response: Resources.guard.extend({ scopes: Scopes.guard.array().optional() }), status: [201, 422], }), @@ -128,7 +131,9 @@ export default function resourceRoutes( '/resources/:id', koaGuard({ params: object({ id: string().min(1) }), - body: Resources.createGuard.omit({ id: true, indicator: true }).partial(), + // Intentionally omit `isDefault` since it'll affect other rows. + // Use the dedicated API `PATCH /resources/:id/is-default` to update. + body: Resources.createGuard.omit({ id: true, indicator: true, isDefault: true }).partial(), response: Resources.guard, status: [200, 404], }), @@ -145,6 +150,27 @@ export default function resourceRoutes( } ); + router.patch( + '/resources/:id/is-default', + koaGuard({ + params: object({ id: string().min(1) }), + body: object({ isDefault: boolean() }), + response: Resources.guard, + status: [200, 404], + }), + async (ctx, next) => { + const { + params: { id }, + body: { isDefault }, + } = ctx.guard; + + // Only 0 or 1 default resource is allowed per tenant, so use a dedicated transaction query for setting the default. + ctx.body = await (isDefault ? setDefaultResource(id) : updateResourceById(id, { isDefault })); + + return next(); + } + ); + router.delete( '/resources/:id', koaGuard({ params: object({ id: string().min(1) }), status: [204, 404] }), diff --git a/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts index ef861acbe..575f11230 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'API Identifikator', api_identifier_tip: 'Der eindeutige Identifikator der API Ressource muss eine absolute URI ohne Fragmentbezeichner (#) sein. Entspricht dem Ressourcen Parameter in OAuth 2.0.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'Die API Ressource {{name}} wurde erfolgreich angelegt', api_identifier_placeholder: 'https://dein-api-identifikator/', }; diff --git a/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts index 4cbf42869..88a940458 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'API identifier', api_identifier_tip: 'The unique identifier to the API resource. It must be an absolute URI and has no fragment (#) component. Equals to the resource parameter in OAuth 2.0.', + default_api: 'Default API', + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API.\nWhen a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', api_resource_created: 'The API resource {{name}} has been successfully created', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/es/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/es/translation/admin-console/api-resources.ts index a05cf472f..361e34fbe 100644 --- a/packages/phrases/src/locales/es/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/es/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'Identificador de API', api_identifier_tip: 'El identificador único para el recurso de API. Debe ser una URI absoluta y no tiene componente de fragmento (#). Es igual al parámetro de recurso en OAuth 2.0.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'El recurso de API {{name}} se ha creado correctamente', api_identifier_placeholder: 'https://su-identificador-de-api/', }; diff --git a/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts index 3409f3ad0..f8c577926 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'Identifiant API', api_identifier_tip: "L'identifiant unique de la ressource API. Il doit s'agir d'un URI absolu et ne doit pas comporter de fragment (#). Équivaut au paramètre de ressource dans OAuth 2.0.", + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'La ressource API {{name}} a été créée avec succès.', api_identifier_placeholder: 'https://votre-identifiant-api/', }; diff --git a/packages/phrases/src/locales/it/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/it/translation/admin-console/api-resources.ts index 6cc4457f3..5120345aa 100644 --- a/packages/phrases/src/locales/it/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/it/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'Identificatore API', api_identifier_tip: "L'identificatore univoco della risorsa API. Deve essere un URI assoluto e non ha componenti di frammento (#). Corrisponde al parametro risorsa in OAuth 2.0.", + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'La risorsa API {{name}} è stata creata con successo', api_identifier_placeholder: 'https://tuo-identificatore-api/', }; diff --git a/packages/phrases/src/locales/ja/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/ja/translation/admin-console/api-resources.ts index f2668d621..555a8cc38 100644 --- a/packages/phrases/src/locales/ja/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/ja/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'API 識別子', api_identifier_tip: 'API リソースの一意の識別子です。絶対URIで、フラグメント(#)コンポーネントはありません。OAuth 2.0でのresource parameterに等しいです。', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'APIリソース{{name}}が正常に作成されました', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts index 2818fc0a5..7f08a89a0 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'API 식별자', api_identifier_tip: 'API 리소스에 대한 고유한 식별자예요. 절대 URI여야 하며 조각 (#) 컴포넌트가 없어야 해요. OAuth 2.0의 resource parameter와 같아요.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: '{{name}} API 리소스가 성공적으로 생성되었어요.', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/api-resources.ts index f54576cbc..3209f5351 100644 --- a/packages/phrases/src/locales/pl-pl/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'Identyfikator API', api_identifier_tip: 'Unikalny identyfikator zasobu API. Musi to być bezwzględny adres URI bez składnika fragmentu (#). Jest równy parametrowi zasobu w standardzie OAuth 2.0.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'Zasób API {{name}} został pomyślnie utworzony', api_identifier_placeholder: 'https://identyfikator-twojego-api/', }; diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts index 284ca6083..14f831150 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'Identificador de API', api_identifier_tip: 'O identificador exclusivo para o recurso da API. Deve ser um URI absoluto e não tem nenhum componente de fragmento (#). Igual ao parâmetro de recurso em OAuth 2.0.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'O recurso API {{name}} foi criado com sucesso', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts index 89354aedb..729797f0f 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'identificador da API', api_identifier_tip: 'O identificador exclusivo para o recurso API. Deve ser um URI absoluto e não tem componente de fragmento (#). Igual ao resource parameter no OAuth 2.0.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'O recurso API {{name}} foi criado com sucesso', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/ru/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/ru/translation/admin-console/api-resources.ts index 1ebfad6b8..7f03ecd9b 100644 --- a/packages/phrases/src/locales/ru/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/ru/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'Идентификатор API', api_identifier_tip: 'Уникальный идентификатор для ресурса API. Он должен быть абсолютным URI и не иметь фрагмента (#). Равен параметру resource в OAuth 2.0.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: 'Ресурс API {{name}} был успешно создан', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts index 3fd22c97f..ec50760ce 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts @@ -8,6 +8,9 @@ const api_resources = { api_identifier: 'API belirteci', api_identifier_tip: 'Api kaynağına özgün belirteç. Mutlak URI olmalı ve parça bileşeni (#) içermemeli. OAuth 2.0deki kaynak parametresine eşittir.', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED api_resource_created: '{{name}} API kaynağı başarıyla oluşturuldu', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts index 61cd532b2..ce6792a48 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts @@ -10,6 +10,9 @@ const api_resources = { api_identifier_tip: '对于 API 资源的唯一标识符。它必须是一个绝对 URI 并没有 fragment (#) 组件。等价于 OAuth 2.0 中的 resource parameter。', api_resource_created: ' API 资源 {{name}} 已成功创建。', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED }; export default api_resources; diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/api-resources.ts index 9d9186b34..7c069342f 100644 --- a/packages/phrases/src/locales/zh-hk/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/api-resources.ts @@ -10,6 +10,9 @@ const api_resources = { api_identifier_tip: '對於 API 資源的唯一標識符。它必須是一個絕對 URI 並沒有 fragment (#) 組件。等價於 OAuth 2.0 中的 resource parameter。', api_resource_created: ' API 資源 {{name}} 已成功創建。', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED }; export default api_resources; diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/api-resources.ts index 8870dec18..e62c72312 100644 --- a/packages/phrases/src/locales/zh-tw/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/api-resources.ts @@ -10,6 +10,9 @@ const api_resources = { api_identifier_tip: '對於 API 資源的唯一標識符。它必須是一個絕對 URI,並沒有 fragment (#) 組件。等價於 OAuth 2.0 中的 resource parameter。', api_resource_created: ' API 資源 {{name}} 已成功創建。', + default_api: 'Default API', // UNTRANSLATED + default_api_label: + 'If the current API Resource is set as the default API for the tenant, while each tenant can have either 0 or 1 default API. When a default API is designated, the resource parameter can be omitted in the auth request. Subsequent token exchanges will use that API as the audience by default, resulting in the issuance of JWTs.', // UNTRANSLATED }; export default api_resources; diff --git a/packages/schemas/alterations/next-1685285719-support-default-resource.ts b/packages/schemas/alterations/next-1685285719-support-default-resource.ts new file mode 100644 index 000000000..4df058bce --- /dev/null +++ b/packages/schemas/alterations/next-1685285719-support-default-resource.ts @@ -0,0 +1,23 @@ +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table resources + add column is_default boolean not null default (false); + create unique index resources__is_default_true + on resources (tenant_id) + where is_default = true; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table resources + drop is_default; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/tables/resources.sql b/packages/schemas/tables/resources.sql index ea16c6300..2ea36a0f9 100644 --- a/packages/schemas/tables/resources.sql +++ b/packages/schemas/tables/resources.sql @@ -6,6 +6,7 @@ create table resources ( id varchar(21) not null, name text not null, indicator text not null, /* resource indicator also used as audience */ + is_default boolean not null default (false), access_token_ttl bigint not null default(3600), /* expiration value in seconds, default is 1h */ primary key (id), constraint resources__indicator @@ -14,3 +15,7 @@ create table resources ( create index resources__id on resources (tenant_id, id); + +create unique index resources__is_default_true + on resources (tenant_id) + where is_default = true;