mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(core,phrases): update applications api to support third-party app (#5096)
update applications api to support third-party app
This commit is contained in:
parent
a93a39aa1b
commit
dcc226b5d9
21 changed files with 157 additions and 53 deletions
|
@ -21,7 +21,6 @@ const pool = createMockPool({
|
|||
const { createApplicationQueries } = await import('./application.js');
|
||||
const {
|
||||
findTotalNumberOfApplications,
|
||||
findAllApplications,
|
||||
findApplicationById,
|
||||
insertApplication,
|
||||
updateApplicationById,
|
||||
|
@ -47,29 +46,6 @@ describe('application query', () => {
|
|||
await expect(findTotalNumberOfApplications()).resolves.toEqual({ count: 10 });
|
||||
});
|
||||
|
||||
it('findAllApplications', async () => {
|
||||
const limit = 10;
|
||||
const offset = 1;
|
||||
const rowData = { id: 'foo' };
|
||||
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
order by "created_at" desc
|
||||
limit $1
|
||||
offset $2
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([limit, offset]);
|
||||
|
||||
return createMockQueryResult([rowData]);
|
||||
});
|
||||
|
||||
await expect(findAllApplications(limit, offset)).resolves.toEqual([rowData]);
|
||||
});
|
||||
|
||||
it('findApplicationById', async () => {
|
||||
const id = 'foo';
|
||||
const rowData = { id };
|
||||
|
|
|
@ -5,7 +5,6 @@ import { convertToIdentifiers, conditionalSql, conditionalArraySql } from '@logt
|
|||
import type { CommonQueryMethods, SqlSqlToken } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import { buildFindAllEntitiesWithPool } from '#src/database/find-all-entities.js';
|
||||
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';
|
||||
|
@ -49,12 +48,14 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
*
|
||||
* @param search The search config object, can apply to `id`, `name` and `description` field for application.
|
||||
* @param excludeApplicationIds Exclude applications with these ids.
|
||||
* @param isThirdParty Optional boolean, filter applications by whether it is a third party application.
|
||||
* @param types Optional array of {@link ApplicationType}, filter applications by types, if not provided, all types will be included.
|
||||
* @returns A Promise that resolves the number of applications that match the search conditions.
|
||||
*/
|
||||
const countApplications = async (
|
||||
search: Search,
|
||||
excludeApplicationIds: string[],
|
||||
isThirdParty?: boolean,
|
||||
types?: ApplicationType[]
|
||||
) => {
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
|
@ -65,6 +66,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
? sql`${fields.id} not in (${sql.join(excludeApplicationIds, sql`, `)})`
|
||||
: sql``,
|
||||
types && types.length > 0 ? sql`${fields.type} in (${sql.join(types, sql`, `)})` : sql``,
|
||||
isThirdParty ? sql`${fields.isThirdParty} = true` : sql`${fields.isThirdParty} = false`,
|
||||
buildApplicationConditions(search),
|
||||
])}
|
||||
`);
|
||||
|
@ -75,20 +77,32 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
/**
|
||||
* Get the list of applications that match the search conditions, conditions are joined in `and` mode.
|
||||
*
|
||||
* @param search The search config object, can apply to `id`, `name` and `description` field for application
|
||||
* @param excludeApplicationIds Exclude applications with these ids.
|
||||
* @param types Optional array of {@link ApplicationType}, filter applications by types, if not provided, all types will be included.
|
||||
* @param limit Limit of the number of applications in each page.
|
||||
* @param offset Offset of the applications in the result.
|
||||
* @param conditions The conditions to filter applications.
|
||||
* @param conditions.search The search config object, can apply to `id`, `name` and `description` field for application
|
||||
* @param conditions.excludeApplicationIds Exclude applications with these ids.
|
||||
* @param conditions.types Optional array of {@link ApplicationType}, filter applications by types, if not provided, all types will be included.
|
||||
* @param conditions.isThirdParty Optional boolean, filter applications by whether it is a third party application.
|
||||
* @param conditions.pagination Optional pagination config object.
|
||||
* @param conditions.pagination.limit The number of applications to return.
|
||||
* @param conditions.pagination.offset The offset of applications to return.
|
||||
* @returns A Promise that resolves the list of applications that match the search conditions.
|
||||
*/
|
||||
const findApplications = async (
|
||||
search: Search,
|
||||
excludeApplicationIds: string[],
|
||||
types?: ApplicationType[],
|
||||
limit?: number,
|
||||
offset?: number
|
||||
) =>
|
||||
const findApplications = async ({
|
||||
search,
|
||||
excludeApplicationIds,
|
||||
types,
|
||||
isThirdParty,
|
||||
pagination,
|
||||
}: {
|
||||
search: Search;
|
||||
excludeApplicationIds: string[];
|
||||
types?: ApplicationType[];
|
||||
isThirdParty?: boolean;
|
||||
pagination?: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
}) =>
|
||||
pool.any<Application>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
|
@ -97,19 +111,16 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
? sql`${fields.id} not in (${sql.join(excludeApplicationIds, sql`, `)})`
|
||||
: sql``,
|
||||
types && types.length > 0 ? sql`${fields.type} in (${sql.join(types, sql`, `)})` : sql``,
|
||||
isThirdParty ? sql`${fields.isThirdParty} = true` : sql`${fields.isThirdParty} = false`,
|
||||
buildApplicationConditions(search),
|
||||
])}
|
||||
order by ${fields.createdAt} desc
|
||||
${conditionalSql(limit, (value) => sql`limit ${value}`)}
|
||||
${conditionalSql(offset, (value) => sql`offset ${value}`)}
|
||||
${conditionalSql(pagination?.limit, (value) => sql`limit ${value}`)}
|
||||
${conditionalSql(pagination?.offset, (value) => sql`offset ${value}`)}
|
||||
`);
|
||||
|
||||
const findTotalNumberOfApplications = async () => getTotalRowCountWithPool(pool)(table);
|
||||
|
||||
const findAllApplications = buildFindAllEntitiesWithPool(pool)(Applications, [
|
||||
{ field: 'createdAt', order: 'desc' },
|
||||
]);
|
||||
|
||||
const findApplicationById = buildFindEntityByIdWithPool(pool)(Applications);
|
||||
|
||||
const insertApplication = buildInsertIntoWithPool(pool)(Applications, {
|
||||
|
@ -212,7 +223,6 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
countApplications,
|
||||
findApplications,
|
||||
findTotalNumberOfApplications,
|
||||
findAllApplications,
|
||||
findApplicationById,
|
||||
insertApplication,
|
||||
updateApplication,
|
||||
|
|
|
@ -7,8 +7,10 @@ import {
|
|||
applicationPatchGuard,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId, generateStandardSecret } from '@logto/shared';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { boolean, object, string, z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
|
@ -64,6 +66,11 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
.array()
|
||||
.or(applicationTypeGuard.transform((type) => [type]))
|
||||
.optional(),
|
||||
excludeRoleId: string().optional(),
|
||||
// FIXME: @simeng-li Remove this guard once Logto as IdP is ready
|
||||
...conditional(
|
||||
EnvSet.values.isDevFeaturesEnabled && { isThirdParty: z.literal('true').optional() }
|
||||
),
|
||||
}),
|
||||
response: z.array(applicationResponseGuard),
|
||||
status: 200,
|
||||
|
@ -71,27 +78,37 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
async (ctx, next) => {
|
||||
const { limit, offset, disabled: paginationDisabled } = ctx.pagination;
|
||||
const { searchParams } = ctx.URL;
|
||||
const { types } = ctx.guard.query;
|
||||
const { types, excludeRoleId } = ctx.guard.query;
|
||||
|
||||
// FIXME: @simeng-li Remove this guard once Logto as IdP is ready
|
||||
const isThirdParty = Boolean(ctx.guard.query.isThirdParty);
|
||||
|
||||
// This will only parse the `search` query param, other params will be ignored. Please use query guard to validate them.
|
||||
const search = parseSearchParamsForSearch(searchParams);
|
||||
|
||||
const excludeRoleId = searchParams.get('excludeRoleId');
|
||||
const excludeApplicationsRoles = excludeRoleId
|
||||
? await findApplicationsRolesByRoleId(excludeRoleId)
|
||||
: [];
|
||||
|
||||
const excludeApplicationIds = excludeApplicationsRoles.map(
|
||||
({ applicationId }) => applicationId
|
||||
);
|
||||
|
||||
if (paginationDisabled) {
|
||||
ctx.body = await findApplications(search, excludeApplicationIds, types);
|
||||
ctx.body = await findApplications({ search, excludeApplicationIds, types, isThirdParty });
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
const [{ count }, applications] = await Promise.all([
|
||||
countApplications(search, excludeApplicationIds, types),
|
||||
findApplications(search, excludeApplicationIds, types, limit, offset),
|
||||
countApplications(search, excludeApplicationIds, isThirdParty, types),
|
||||
findApplications({
|
||||
search,
|
||||
excludeApplicationIds,
|
||||
types,
|
||||
isThirdParty,
|
||||
pagination: { limit, offset },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Return totalCount to pagination middleware
|
||||
|
@ -118,6 +135,14 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
: 'applicationsLimit'
|
||||
);
|
||||
|
||||
// Third party applications must be traditional type
|
||||
if (rest.isThirdParty) {
|
||||
assertThat(
|
||||
rest.type === ApplicationType.Traditional,
|
||||
'application.invalid_third_party_application_type'
|
||||
);
|
||||
}
|
||||
|
||||
ctx.body = await insertApplication({
|
||||
id: generateStandardId(),
|
||||
secret: generateStandardSecret(),
|
||||
|
|
|
@ -6,10 +6,14 @@ import {
|
|||
import { EnvSet } from '#src/env-set/index.js';
|
||||
|
||||
// FIXME: @simeng-li Remove this guard once Logto as IdP is ready
|
||||
export const applicationResponseGuard = EnvSet.values.isDevFeaturesEnabled
|
||||
// @ts-expect-error -- hide the dev feature field from the guard type, but always return the full type to make the api logic simpler
|
||||
export const applicationResponseGuard: typeof Applications.guard = EnvSet.values
|
||||
.isDevFeaturesEnabled
|
||||
? Applications.guard
|
||||
: Applications.guard.omit({ isThirdParty: true });
|
||||
|
||||
export const applicationCreateGuard = EnvSet.values.isDevFeaturesEnabled
|
||||
// @ts-expect-error -- hide the dev feature field from the guard type, but always return the full type to make the api logic simpler
|
||||
export const applicationCreateGuard: typeof originalApplicationCreateGuard = EnvSet.values
|
||||
.isDevFeaturesEnabled
|
||||
? originalApplicationCreateGuard
|
||||
: originalApplicationCreateGuard.omit({ isThirdParty: true });
|
||||
|
|
|
@ -26,7 +26,8 @@ export const createApplication = async (
|
|||
|
||||
export const getApplications = async (
|
||||
types?: ApplicationType[],
|
||||
searchParameters?: Record<string, string>
|
||||
searchParameters?: Record<string, string>,
|
||||
isThirdParty?: boolean
|
||||
) => {
|
||||
const searchParams = new URLSearchParams([
|
||||
...(conditional(types && types.length > 0 && types.map((type) => ['types', type])) ?? []),
|
||||
|
@ -35,6 +36,7 @@ export const getApplications = async (
|
|||
Object.keys(searchParameters).length > 0 &&
|
||||
Object.entries(searchParameters).map(([key, value]) => [key, value])
|
||||
) ?? []),
|
||||
...(conditional(isThirdParty && [['isThirdParty', 'true']]) ?? []),
|
||||
]);
|
||||
|
||||
return authedAdminApi.get('applications', { searchParams }).json<Application[]>();
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
deleteApplication,
|
||||
getApplications,
|
||||
} from '#src/api/index.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
|
||||
describe('admin console application', () => {
|
||||
it('should create application successfully', async () => {
|
||||
|
@ -17,6 +18,7 @@ describe('admin console application', () => {
|
|||
|
||||
expect(application.name).toBe(applicationName);
|
||||
expect(application.type).toBe(applicationType);
|
||||
expect(application.isThirdParty).toBe(false);
|
||||
|
||||
const fetchedApplication = await getApplication(application.id);
|
||||
|
||||
|
@ -24,6 +26,28 @@ describe('admin console application', () => {
|
|||
expect(fetchedApplication.id).toBe(application.id);
|
||||
});
|
||||
|
||||
it('should throw error when creating a third party application with invalid type', async () => {
|
||||
await expectRejects(
|
||||
createApplication('test-create-app', ApplicationType.Native, {
|
||||
isThirdParty: true,
|
||||
}),
|
||||
{ code: 'application.invalid_third_party_application_type', statusCode: 400 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should create third party application successfully', async () => {
|
||||
const applicationName = 'test-third-party-app';
|
||||
|
||||
const application = await createApplication(applicationName, ApplicationType.Traditional, {
|
||||
isThirdParty: true,
|
||||
});
|
||||
|
||||
expect(application.name).toBe(applicationName);
|
||||
expect(application.type).toBe(ApplicationType.Traditional);
|
||||
expect(application.isThirdParty).toBe(true);
|
||||
await deleteApplication(application.id);
|
||||
});
|
||||
|
||||
it('should update application details successfully', async () => {
|
||||
const application = await createApplication('test-update-app', ApplicationType.Traditional);
|
||||
|
||||
|
@ -74,11 +98,30 @@ describe('admin console application', () => {
|
|||
expect(application.id).toBe('demo-app');
|
||||
});
|
||||
|
||||
it('should fetch all applications created above', async () => {
|
||||
it('should fetch all non-third party applications created above', async () => {
|
||||
const applications = await getApplications();
|
||||
|
||||
const applicationNames = applications.map(({ name }) => name);
|
||||
expect(applicationNames).toContain('test-create-app');
|
||||
expect(applicationNames).toContain('test-update-app');
|
||||
|
||||
expect(applications.some(({ isThirdParty }) => isThirdParty)).toBe(false);
|
||||
});
|
||||
|
||||
it('should fetch all third party applications created', async () => {
|
||||
const application = await createApplication(
|
||||
'test-third-party-app',
|
||||
ApplicationType.Traditional,
|
||||
{
|
||||
isThirdParty: true,
|
||||
}
|
||||
);
|
||||
|
||||
const applications = await getApplications(undefined, undefined, true);
|
||||
|
||||
expect(applications.find(({ id }) => id === application.id)).toEqual(application);
|
||||
expect(applications.some(({ isThirdParty }) => !isThirdParty)).toBe(false);
|
||||
await deleteApplication(application.id);
|
||||
});
|
||||
|
||||
it('should create m2m application successfully and can get only m2m applications by specifying types', async () => {
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: 'Die Rolle mit der ID {{roleId}} wurde bereits dieser Anwendung hinzugefügt.',
|
||||
invalid_role_type:
|
||||
'Es ist nicht möglich, einer Maschinen-zu-Maschinen-Anwendung eine Benutzertyp-Rolle zuzuweisen.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -2,6 +2,8 @@ const application = {
|
|||
invalid_type: 'Only machine to machine applications can have associated roles.',
|
||||
role_exists: 'The role id {{roleId}} is already been added to this application.',
|
||||
invalid_role_type: 'Can not assign user type role to machine to machine application.',
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: 'La identificación del rol {{roleId}} ya se ha agregado a esta aplicación.',
|
||||
invalid_role_type:
|
||||
'No se puede asignar un rol de tipo usuario a una aplicación de máquina a máquina.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: "Le rôle d'identifiant {{roleId}} a déjà été ajouté à cette application.",
|
||||
invalid_role_type:
|
||||
"Impossible d'assigner un rôle de type utilisateur à une application machine à machine.",
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: "L'ID ruolo {{roleId}} è già stato aggiunto a questa applicazione.",
|
||||
invalid_role_type:
|
||||
"Impossibile assegnare un ruolo di tipo utente all'applicazione da macchina a macchina.",
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: 'ロールID {{roleId}} は、すでにこのアプリケーションに追加されています。',
|
||||
invalid_role_type:
|
||||
'ユーザータイプのロールをマシン間アプリケーションに割り当てることはできません。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -2,6 +2,9 @@ const application = {
|
|||
invalid_type: '관련 역할을 가질 수 있는 것은 기계 대 기계 응용 프로그램만 가능합니다.',
|
||||
role_exists: '역할 ID {{roleId}} 가 이미이 응용 프로그램에 추가되었습니다.',
|
||||
invalid_role_type: '사용자 유형 역할을 기계 대 기계 응용 프로그램에 할당할 수 없습니다.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -2,6 +2,9 @@ const application = {
|
|||
invalid_type: 'Tylko aplikacje maszyna-do-maszyny mogą mieć przypisane role.',
|
||||
role_exists: 'Rola o identyfikatorze {{roleId}} została już dodana do tej aplikacji.',
|
||||
invalid_role_type: 'Nie można przypisać roli typu użytkownika do aplikacji maszyna-do-maszyny.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: 'O id da função {{roleId}} já foi adicionado a este aplicativo.',
|
||||
invalid_role_type:
|
||||
'Não é possível atribuir uma função de tipo de usuário a um aplicativo de máquina para máquina.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: 'O id da função {{roleId}} já foi adicionado a esta aplicação.',
|
||||
invalid_role_type:
|
||||
'Não é possível atribuir uma função de tipo de utilizador a uma aplicação máquina a máquina.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -3,6 +3,9 @@ const application = {
|
|||
role_exists: 'Роль с идентификатором {{roleId}} уже добавлена в это приложение.',
|
||||
invalid_role_type:
|
||||
'Невозможно назначить роль типа "пользователь" для приложения типа "от машины к машине".',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -2,6 +2,9 @@ const application = {
|
|||
invalid_type: 'Sadece makine ile makine uygulamaları rollerle ilişkilendirilebilir.',
|
||||
role_exists: 'Bu uygulamaya zaten {{roleId}} kimlikli bir rol eklenmiş.',
|
||||
invalid_role_type: 'Kullanıcı tipi rolü makine ile makine uygulamasına atayamaz.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -2,6 +2,9 @@ const application = {
|
|||
invalid_type: '只有机器对机器应用程序可以有关联角色。',
|
||||
role_exists: '角色 ID {{roleId}} 已添加到此应用程序。',
|
||||
invalid_role_type: '无法将用户类型角色分配给机器对机器应用程序。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -2,6 +2,9 @@ const application = {
|
|||
invalid_type: '只有機器對機器應用程式才能有相關職能。',
|
||||
role_exists: '角色 ID {{roleId}} 已經被添加到此應用程式中。',
|
||||
invalid_role_type: '無法將使用者類型的角色分配給機器對機器應用程式。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -2,6 +2,9 @@ const application = {
|
|||
invalid_type: '僅允許機器對機器應用程式附加角色。',
|
||||
role_exists: '該角色 ID {{roleId}} 已被添加至此應用程式。',
|
||||
invalid_role_type: '無法將使用者類型的角色指派給機器對機器應用程式。',
|
||||
/** UNTRANSLATED */
|
||||
invalid_third_party_application_type:
|
||||
'Only traditional web applications can be marked as a third-party app.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
Loading…
Add table
Reference in a new issue