0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat: support default API Resource

This commit is contained in:
Gao Sun 2023-05-29 18:44:56 +08:00
parent 0dbacb89aa
commit c933bf58f7
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
22 changed files with 160 additions and 33 deletions

View file

@ -7,6 +7,7 @@ import { useOutletContext } from 'react-router-dom';
import DetailsForm from '@/components/DetailsForm'; import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard'; import FormCard from '@/components/FormCard';
import FormField from '@/components/FormField'; import FormField from '@/components/FormField';
import Switch from '@/components/Switch';
import TextInput from '@/components/TextInput'; import TextInput from '@/components/TextInput';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
@ -32,14 +33,20 @@ function ApiResourceSettings() {
const api = useApi(); const api = useApi();
const onSubmit = handleSubmit(async (formData) => { const onSubmit = handleSubmit(async ({ isDefault, ...rest }) => {
if (isSubmitting) { if (isSubmitting) {
return; return;
} }
const updatedApiResource = await api const [data] = await Promise.all([
.patch(`api/resources/${resource.id}`, { json: formData }) api.patch(`api/resources/${resource.id}`, { json: rest }).json<Resource>(),
.json<Resource>(); api
.patch(`api/resources/${resource.id}/is-default`, { json: { isDefault } })
.json<Resource>(),
]);
// We cannot ensure the order of API requests, manually combine the results
const updatedApiResource = { ...data, isDefault };
reset(updatedApiResource); reset(updatedApiResource);
onResourceUpdated(updatedApiResource); onResourceUpdated(updatedApiResource);
toast.success(t('general.saved')); toast.success(t('general.saved'));
@ -77,6 +84,11 @@ function ApiResourceSettings() {
placeholder={t('api_resource_details.token_expiration_time_in_seconds_placeholder')} placeholder={t('api_resource_details.token_expiration_time_in_seconds_placeholder')}
/> />
</FormField> </FormField>
{!isLogtoManagementApiResource && (
<FormField title="api_resources.default_api">
<Switch {...register('isDefault')} label={t('api_resources.default_api_label')} />
</FormField>
)}
</FormCard> </FormCard>
</DetailsForm> </DetailsForm>
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} /> <UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />

View file

@ -3,14 +3,8 @@ import type { OmitAutoSetFields, UpdateWhereData } from '@logto/shared';
import { SlonikError } from 'slonik'; import { SlonikError } from 'slonik';
export class DeletionError extends SlonikError { export class DeletionError extends SlonikError {
table?: string; public constructor(public readonly table?: string, public readonly id?: string) {
id?: string;
public constructor(table?: string, id?: string) {
super('Resource not found.'); super('Resource not found.');
this.table = table;
this.id = id;
} }
} }
@ -18,17 +12,11 @@ export class UpdateError<
CreateSchema extends SchemaLike, CreateSchema extends SchemaLike,
Schema extends CreateSchema Schema extends CreateSchema
> extends SlonikError { > extends SlonikError {
schema: GeneratedSchema<CreateSchema, Schema>;
detail: UpdateWhereData<Schema>;
public constructor( public constructor(
schema: GeneratedSchema<CreateSchema, Schema>, public readonly schema: GeneratedSchema<CreateSchema, Schema>,
detail: UpdateWhereData<Schema> public readonly detail: Partial<UpdateWhereData<Schema>>
) { ) {
super('Resource not found.'); super('Resource not found.');
this.schema = schema;
this.detail = detail;
} }
} }
@ -36,16 +24,10 @@ export class InsertionError<
CreateSchema extends SchemaLike, CreateSchema extends SchemaLike,
Schema extends CreateSchema Schema extends CreateSchema
> extends SlonikError { > extends SlonikError {
schema: GeneratedSchema<CreateSchema, Schema>;
detail?: OmitAutoSetFields<CreateSchema>;
public constructor( public constructor(
schema: GeneratedSchema<CreateSchema, Schema>, public readonly schema: GeneratedSchema<CreateSchema, Schema>,
detail?: OmitAutoSetFields<CreateSchema> public readonly detail?: OmitAutoSetFields<CreateSchema>
) { ) {
super('Create Error.'); super('Create Error.');
this.schema = schema;
this.detail = detail;
} }
} }

View file

@ -48,7 +48,7 @@ export default function initOidc(
defaultRefreshTokenTtl, defaultRefreshTokenTtl,
} = envSet.oidc; } = envSet.oidc;
const { const {
resources: { findResourceByIndicator }, resources: { findResourceByIndicator, findDefaultResource },
users: { findUserById }, users: { findUserById },
} = queries; } = queries;
const { findUserScopesForResourceIndicator } = libraries.users; 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 // https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#featuresresourceindicators
resourceIndicators: { resourceIndicators: {
enabled: true, enabled: true,
defaultResource: () => '', defaultResource: async () => {
const resource = await findDefaultResource();
return resource?.indicator ?? '';
},
// Disable the auto use of authorization_code granted resource feature // Disable the auto use of authorization_code granted resource feature
useGrantedResource: () => false, useGrantedResource: () => false,
getResourceServerInfo: async (ctx, indicator) => { getResourceServerInfo: async (ctx, indicator) => {

View file

@ -10,7 +10,7 @@ import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js'
import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
import { getTotalRowCountWithPool } from '#src/database/row-count.js'; import { getTotalRowCountWithPool } from '#src/database/row-count.js';
import { buildUpdateWhereWithPool } from '#src/database/update-where.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); const { table, fields } = convertToIdentifiers(Resources);
@ -26,6 +26,35 @@ export const createResourceQueries = (pool: CommonQueryMethods) => {
where ${fields.indicator}=${indicator} where ${fields.indicator}=${indicator}
`); `);
const findDefaultResource = async () =>
pool.maybeOne<Resource>(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<Resource>(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 findResourceById = buildFindEntityByIdWithPool(pool)(Resources);
const findResourcesByIds = async (resourceIds: string[]) => const findResourcesByIds = async (resourceIds: string[]) =>
@ -64,6 +93,8 @@ export const createResourceQueries = (pool: CommonQueryMethods) => {
findTotalNumberOfResources, findTotalNumberOfResources,
findAllResources, findAllResources,
findResourceByIndicator, findResourceByIndicator,
findDefaultResource,
setDefaultResource,
findResourceById, findResourceById,
findResourcesByIds, findResourcesByIds,
insertResource, insertResource,

View file

@ -1,7 +1,7 @@
import { Resources, Scopes } from '@logto/schemas'; import { Resources, Scopes } from '@logto/schemas';
import { buildIdGenerator } from '@logto/shared'; import { buildIdGenerator } from '@logto/shared';
import { tryThat, yes } from '@silverhand/essentials'; 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 RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js'; import koaGuard from '#src/middleware/koa-guard.js';
@ -23,6 +23,7 @@ export default function resourceRoutes<T extends AuthedRouter>(
findAllResources, findAllResources,
findResourceById, findResourceById,
findResourceByIndicator, findResourceByIndicator,
setDefaultResource,
insertResource, insertResource,
updateResourceById, updateResourceById,
deleteResourceById, deleteResourceById,
@ -76,7 +77,9 @@ export default function resourceRoutes<T extends AuthedRouter>(
router.post( router.post(
'/resources', '/resources',
koaGuard({ 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() }), response: Resources.guard.extend({ scopes: Scopes.guard.array().optional() }),
status: [201, 422], status: [201, 422],
}), }),
@ -128,7 +131,9 @@ export default function resourceRoutes<T extends AuthedRouter>(
'/resources/:id', '/resources/:id',
koaGuard({ koaGuard({
params: object({ id: string().min(1) }), 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, response: Resources.guard,
status: [200, 404], status: [200, 404],
}), }),
@ -145,6 +150,27 @@ export default function resourceRoutes<T extends AuthedRouter>(
} }
); );
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( router.delete(
'/resources/:id', '/resources/:id',
koaGuard({ params: object({ id: string().min(1) }), status: [204, 404] }), koaGuard({ params: object({ id: string().min(1) }), status: [204, 404] }),

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'API Identifikator', api_identifier: 'API Identifikator',
api_identifier_tip: api_identifier_tip:
'Der eindeutige Identifikator der API Ressource muss eine absolute URI ohne Fragmentbezeichner (#) sein. Entspricht dem <a>Ressourcen Parameter</a> in OAuth 2.0.', 'Der eindeutige Identifikator der API Ressource muss eine absolute URI ohne Fragmentbezeichner (#) sein. Entspricht dem <a>Ressourcen Parameter</a> 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_resource_created: 'Die API Ressource {{name}} wurde erfolgreich angelegt',
api_identifier_placeholder: 'https://dein-api-identifikator/', api_identifier_placeholder: 'https://dein-api-identifikator/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'API identifier', api_identifier: 'API identifier',
api_identifier_tip: api_identifier_tip:
'The unique identifier to the API resource. It must be an absolute URI and has no fragment (#) component. Equals to the <a>resource parameter</a> in OAuth 2.0.', 'The unique identifier to the API resource. It must be an absolute URI and has no fragment (#) component. Equals to the <a>resource parameter</a> 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_resource_created: 'The API resource {{name}} has been successfully created',
api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_placeholder: 'https://your-api-identifier/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'Identificador de API', api_identifier: 'Identificador de API',
api_identifier_tip: 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 <a>parámetro de recurso</a> en OAuth 2.0.', 'El identificador único para el recurso de API. Debe ser una URI absoluta y no tiene componente de fragmento (#). Es igual al <a>parámetro de recurso</a> 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_resource_created: 'El recurso de API {{name}} se ha creado correctamente',
api_identifier_placeholder: 'https://su-identificador-de-api/', api_identifier_placeholder: 'https://su-identificador-de-api/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'Identifiant API', api_identifier: 'Identifiant API',
api_identifier_tip: 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 <a>paramètre de ressource</> dans OAuth 2.0.", "L'identifiant unique de la ressource API. Il doit s'agir d'un URI absolu et ne doit pas comporter de fragment (#). Équivaut au <a>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_resource_created: 'La ressource API {{name}} a été créée avec succès.',
api_identifier_placeholder: 'https://votre-identifiant-api/', api_identifier_placeholder: 'https://votre-identifiant-api/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'Identificatore API', api_identifier: 'Identificatore API',
api_identifier_tip: api_identifier_tip:
"L'identificatore univoco della risorsa API. Deve essere un URI assoluto e non ha componenti di frammento (#). Corrisponde al parametro <a>risorsa</a> in OAuth 2.0.", "L'identificatore univoco della risorsa API. Deve essere un URI assoluto e non ha componenti di frammento (#). Corrisponde al parametro <a>risorsa</a> 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_resource_created: 'La risorsa API {{name}} è stata creata con successo',
api_identifier_placeholder: 'https://tuo-identificatore-api/', api_identifier_placeholder: 'https://tuo-identificatore-api/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'API 識別子', api_identifier: 'API 識別子',
api_identifier_tip: api_identifier_tip:
'API リソースの一意の識別子です。絶対URIで、フラグメント(#)コンポーネントはありません。OAuth 2.0での<a>resource parameter</a>に等しいです。', 'API リソースの一意の識別子です。絶対URIで、フラグメント(#)コンポーネントはありません。OAuth 2.0での<a>resource parameter</a>に等しいです。',
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_resource_created: 'APIリソース{{name}}が正常に作成されました',
api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_placeholder: 'https://your-api-identifier/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'API 식별자', api_identifier: 'API 식별자',
api_identifier_tip: api_identifier_tip:
'API 리소스에 대한 고유한 식별자예요. 절대 URI여야 하며 조각 (#) 컴포넌트가 없어야 해요. OAuth 2.0의 <a>resource parameter</a>와 같아요.', 'API 리소스에 대한 고유한 식별자예요. 절대 URI여야 하며 조각 (#) 컴포넌트가 없어야 해요. OAuth 2.0의 <a>resource parameter</a>와 같아요.',
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_resource_created: '{{name}} API 리소스가 성공적으로 생성되었어요.',
api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_placeholder: 'https://your-api-identifier/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'Identyfikator API', api_identifier: 'Identyfikator API',
api_identifier_tip: api_identifier_tip:
'Unikalny identyfikator zasobu API. Musi to być bezwzględny adres URI bez składnika fragmentu (#). Jest równy <a>parametrowi zasobu</a> w standardzie OAuth 2.0.', 'Unikalny identyfikator zasobu API. Musi to być bezwzględny adres URI bez składnika fragmentu (#). Jest równy <a>parametrowi zasobu</a> 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_resource_created: 'Zasób API {{name}} został pomyślnie utworzony',
api_identifier_placeholder: 'https://identyfikator-twojego-api/', api_identifier_placeholder: 'https://identyfikator-twojego-api/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'Identificador de API', api_identifier: 'Identificador de API',
api_identifier_tip: 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 <a>parâmetro de recurso</a> em OAuth 2.0.', 'O identificador exclusivo para o recurso da API. Deve ser um URI absoluto e não tem nenhum componente de fragmento (#). Igual ao <a>parâmetro de recurso</a> 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_resource_created: 'O recurso API {{name}} foi criado com sucesso',
api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_placeholder: 'https://your-api-identifier/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'identificador da API', api_identifier: 'identificador da API',
api_identifier_tip: api_identifier_tip:
'O identificador exclusivo para o recurso API. Deve ser um URI absoluto e não tem componente de fragmento (#). Igual ao <a>resource parameter</a> no OAuth 2.0.', 'O identificador exclusivo para o recurso API. Deve ser um URI absoluto e não tem componente de fragmento (#). Igual ao <a>resource parameter</a> 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_resource_created: 'O recurso API {{name}} foi criado com sucesso',
api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_placeholder: 'https://your-api-identifier/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'Идентификатор API', api_identifier: 'Идентификатор API',
api_identifier_tip: api_identifier_tip:
'Уникальный идентификатор для ресурса API. Он должен быть абсолютным URI и не иметь фрагмента (#). Равен параметру <a>resource</a> в OAuth 2.0.', 'Уникальный идентификатор для ресурса API. Он должен быть абсолютным URI и не иметь фрагмента (#). Равен параметру <a>resource</a> в 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_resource_created: 'Ресурс API {{name}} был успешно создан',
api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_placeholder: 'https://your-api-identifier/',
}; };

View file

@ -8,6 +8,9 @@ const api_resources = {
api_identifier: 'API belirteci', api_identifier: 'API belirteci',
api_identifier_tip: api_identifier_tip:
'Api kaynağına özgün belirteç. Mutlak URI olmalı ve parça bileşeni (#) içermemeli. OAuth 2.0deki <a>kaynak parametresine</a> eşittir.', 'Api kaynağına özgün belirteç. Mutlak URI olmalı ve parça bileşeni (#) içermemeli. OAuth 2.0deki <a>kaynak parametresine</a> 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_resource_created: '{{name}} API kaynağı başarıyla oluşturuldu',
api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_placeholder: 'https://your-api-identifier/',
}; };

View file

@ -10,6 +10,9 @@ const api_resources = {
api_identifier_tip: api_identifier_tip:
'对于 API 资源的唯一标识符。它必须是一个绝对 URI 并没有 fragment (#) 组件。等价于 OAuth 2.0 中的 <a>resource parameter</a>。', '对于 API 资源的唯一标识符。它必须是一个绝对 URI 并没有 fragment (#) 组件。等价于 OAuth 2.0 中的 <a>resource parameter</a>。',
api_resource_created: ' API 资源 {{name}} 已成功创建。', 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; export default api_resources;

View file

@ -10,6 +10,9 @@ const api_resources = {
api_identifier_tip: api_identifier_tip:
'對於 API 資源的唯一標識符。它必須是一個絕對 URI 並沒有 fragment (#) 組件。等價於 OAuth 2.0 中的 <a>resource parameter</a>。', '對於 API 資源的唯一標識符。它必須是一個絕對 URI 並沒有 fragment (#) 組件。等價於 OAuth 2.0 中的 <a>resource parameter</a>。',
api_resource_created: ' API 資源 {{name}} 已成功創建。', 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; export default api_resources;

View file

@ -10,6 +10,9 @@ const api_resources = {
api_identifier_tip: api_identifier_tip:
'對於 API 資源的唯一標識符。它必須是一個絕對 URI並沒有 fragment (#) 組件。等價於 OAuth 2.0 中的 <a>resource parameter</a>。', '對於 API 資源的唯一標識符。它必須是一個絕對 URI並沒有 fragment (#) 組件。等價於 OAuth 2.0 中的 <a>resource parameter</a>。',
api_resource_created: ' API 資源 {{name}} 已成功創建。', 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; export default api_resources;

View file

@ -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;

View file

@ -6,6 +6,7 @@ create table resources (
id varchar(21) not null, id varchar(21) not null,
name text not null, name text not null,
indicator text not null, /* resource indicator also used as audience */ 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 */ access_token_ttl bigint not null default(3600), /* expiration value in seconds, default is 1h */
primary key (id), primary key (id),
constraint resources__indicator constraint resources__indicator
@ -14,3 +15,7 @@ create table resources (
create index resources__id create index resources__id
on resources (tenant_id, id); on resources (tenant_id, id);
create unique index resources__is_default_true
on resources (tenant_id)
where is_default = true;