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

feat(core): add role-application related APIs (#4384)

* feat(core): add role-app related APIs

* chore(core): add search for GET role-application
This commit is contained in:
Darcy Ye 2023-09-11 14:04:37 +08:00 committed by GitHub
parent 709ba633b8
commit 285aa745e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 710 additions and 204 deletions

View file

@ -58,6 +58,14 @@ export const mockScopeWithResource = {
resource: mockResource, resource: mockResource,
}; };
export const mockAdminApplicationRole: Role = {
tenantId: 'fake_tenant',
id: 'role_id',
name: 'admin',
description: 'admin application',
type: RoleType.MachineToMachine,
};
export const mockAdminUserRole: Role = { export const mockAdminUserRole: Role = {
tenantId: 'fake_tenant', tenantId: 'fake_tenant',
id: 'role_id', id: 'role_id',

View file

@ -1,7 +1,7 @@
import type { CreateApplication } from '@logto/schemas'; import type { Application, CreateApplication } from '@logto/schemas';
import { ApplicationType, Applications } from '@logto/schemas'; import { ApplicationType, Applications } from '@logto/schemas';
import type { OmitAutoSetFields } from '@logto/shared'; import type { OmitAutoSetFields } from '@logto/shared';
import { convertToIdentifiers } from '@logto/shared'; import { convertToIdentifiers, conditionalSql } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik'; import type { CommonQueryMethods } from 'slonik';
import { sql } from 'slonik'; import { sql } from 'slonik';
@ -11,23 +11,45 @@ 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 } from '#src/errors/SlonikError/index.js';
import { buildConditionsFromSearch } from '#src/utils/search.js';
import type { Search } from '#src/utils/search.js';
const { table, fields } = convertToIdentifiers(Applications); const { table, fields } = convertToIdentifiers(Applications);
const buildApplicationConditions = (search: Search) => {
const hasSearch = search.matches.length > 0;
const searchFields = [
Applications.fields.id,
Applications.fields.name,
Applications.fields.description,
];
return conditionalSql(
hasSearch,
() => sql`and ${buildConditionsFromSearch(search, searchFields)}`
);
};
export const createApplicationQueries = (pool: CommonQueryMethods) => { export const createApplicationQueries = (pool: CommonQueryMethods) => {
const findTotalNumberOfApplications = async () => getTotalRowCountWithPool(pool)(table); const findTotalNumberOfApplications = async () => getTotalRowCountWithPool(pool)(table);
const findAllApplications = buildFindAllEntitiesWithPool(pool)(Applications, [ const findAllApplications = buildFindAllEntitiesWithPool(pool)(Applications, [
{ field: 'createdAt', order: 'desc' }, { field: 'createdAt', order: 'desc' },
]); ]);
const findApplicationById = buildFindEntityByIdWithPool(pool)(Applications); const findApplicationById = buildFindEntityByIdWithPool(pool)(Applications);
const insertApplication = buildInsertIntoWithPool(pool)(Applications, { const insertApplication = buildInsertIntoWithPool(pool)(Applications, {
returning: true, returning: true,
}); });
const updateApplication = buildUpdateWhereWithPool(pool)(Applications, true); const updateApplication = buildUpdateWhereWithPool(pool)(Applications, true);
const updateApplicationById = async ( const updateApplicationById = async (
id: string, id: string,
set: Partial<OmitAutoSetFields<CreateApplication>> set: Partial<OmitAutoSetFields<CreateApplication>>
) => updateApplication({ set, where: { id }, jsonbMode: 'merge' }); ) => updateApplication({ set, where: { id }, jsonbMode: 'merge' });
const countNonM2mApplications = async () => { const countNonM2mApplications = async () => {
const { count } = await pool.one<{ count: string }>(sql` const { count } = await pool.one<{ count: string }>(sql`
select count(*) select count(*)
@ -37,6 +59,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
return { count: Number(count) }; return { count: Number(count) };
}; };
const countM2mApplications = async () => { const countM2mApplications = async () => {
const { count } = await pool.one<{ count: string }>(sql` const { count } = await pool.one<{ count: string }>(sql`
select count(*) select count(*)
@ -47,6 +70,43 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
return { count: Number(count) }; return { count: Number(count) };
}; };
const countM2mApplicationsByIds = async (search: Search, applicationIds: string[]) => {
if (applicationIds.length === 0) {
return { count: 0 };
}
const { count } = await pool.one<{ count: string }>(sql`
select count(*)
from ${table}
where ${fields.type} = ${ApplicationType.MachineToMachine}
and ${fields.id} in (${sql.join(applicationIds, sql`, `)})
${buildApplicationConditions(search)}
`);
return { count: Number(count) };
};
const findM2mApplicationsByIds = async (
search: Search,
limit: number,
offset: number,
applicationIds: string[]
) => {
if (applicationIds.length === 0) {
return [];
}
return pool.any<Application>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.type} = ${ApplicationType.MachineToMachine}
and ${fields.id} in (${sql.join(applicationIds, sql`, `)})
${buildApplicationConditions(search)}
limit ${limit}
offset ${offset}
`);
};
const deleteApplicationById = async (id: string) => { const deleteApplicationById = async (id: string) => {
const { rowCount } = await pool.query(sql` const { rowCount } = await pool.query(sql`
delete from ${table} delete from ${table}
@ -67,6 +127,8 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
updateApplicationById, updateApplicationById,
countNonM2mApplications, countNonM2mApplications,
countM2mApplications, countM2mApplications,
countM2mApplicationsByIds,
findM2mApplicationsByIds,
deleteApplicationById, deleteApplicationById,
}; };
}; };

View file

@ -1,6 +1,7 @@
import type { ApplicationsRole, CreateApplicationsRole, Role } from '@logto/schemas'; import type { ApplicationsRole, CreateApplicationsRole, Role } from '@logto/schemas';
import { Roles, ApplicationsRoles, RolesScopes } from '@logto/schemas'; import { Roles, ApplicationsRoles, RolesScopes } from '@logto/schemas';
import { convertToIdentifiers } from '@logto/shared'; import { convertToIdentifiers } from '@logto/shared';
import { type Nullable } from '@silverhand/essentials';
import type { CommonQueryMethods } from 'slonik'; import type { CommonQueryMethods } from 'slonik';
import { sql } from 'slonik'; import { sql } from 'slonik';
@ -10,6 +11,27 @@ const { table, fields } = convertToIdentifiers(ApplicationsRoles, true);
const { fields: insertFields } = convertToIdentifiers(ApplicationsRoles); const { fields: insertFields } = convertToIdentifiers(ApplicationsRoles);
export const createApplicationsRolesQueries = (pool: CommonQueryMethods) => { export const createApplicationsRolesQueries = (pool: CommonQueryMethods) => {
const findFirstApplicationsRolesByRoleIdAndApplicationIds = async (
roleId: string,
applicationIds: string[]
): Promise<Nullable<ApplicationsRole>> =>
applicationIds.length > 0
? pool.maybeOne<ApplicationsRole>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.roleId}=${roleId}
and ${fields.applicationId} in (${sql.join(applicationIds, sql`, `)})
limit 1
`)
: null;
const countApplicationsRolesByRoleId = async (roleId: string) =>
pool.one<{ count: number }>(sql`
select count(*)
from ${table}
where ${fields.roleId}=${roleId}
`);
const findApplicationsRolesByApplicationId = async (applicationId: string) => const findApplicationsRolesByApplicationId = async (applicationId: string) =>
pool.any<ApplicationsRole & { role: Role }>(sql` pool.any<ApplicationsRole & { role: Role }>(sql`
select select
@ -20,6 +42,14 @@ export const createApplicationsRolesQueries = (pool: CommonQueryMethods) => {
where ${fields.applicationId}=${applicationId} where ${fields.applicationId}=${applicationId}
`); `);
const findApplicationsRolesByRoleId = async (roleId: string) =>
pool.any<ApplicationsRole>(sql`
select
${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.roleId}=${roleId}
`);
const insertApplicationsRoles = async (applicationsRoles: CreateApplicationsRole[]) => const insertApplicationsRoles = async (applicationsRoles: CreateApplicationsRole[]) =>
pool.query(sql` pool.query(sql`
insert into ${table} (${insertFields.id}, ${insertFields.applicationId}, ${ insert into ${table} (${insertFields.id}, ${insertFields.applicationId}, ${
@ -45,7 +75,10 @@ export const createApplicationsRolesQueries = (pool: CommonQueryMethods) => {
}; };
return { return {
findFirstApplicationsRolesByRoleIdAndApplicationIds,
countApplicationsRolesByRoleId,
findApplicationsRolesByApplicationId, findApplicationsRolesByApplicationId,
findApplicationsRolesByRoleId,
insertApplicationsRoles, insertApplicationsRoles,
deleteApplicationRole, deleteApplicationRole,
}; };

View file

@ -24,11 +24,11 @@ const buildRoleConditions = (search: Search) => {
); );
}; };
export const defaultUserSearch = { matches: [], isCaseSensitive: false, joint: SearchJointMode.Or }; export const defaultSearch = { matches: [], isCaseSensitive: false, joint: SearchJointMode.Or };
export const createRolesQueries = (pool: CommonQueryMethods) => { export const createRolesQueries = (pool: CommonQueryMethods) => {
const countRoles = async ( const countRoles = async (
search: Search = defaultUserSearch, search: Search = defaultSearch,
{ excludeRoleIds = [], roleIds }: { excludeRoleIds?: string[]; roleIds?: string[] } = {} { excludeRoleIds = [], roleIds }: { excludeRoleIds?: string[]; roleIds?: string[] } = {}
) => { ) => {
const { count } = await pool.one<{ count: string }>(sql` const { count } = await pool.one<{ count: string }>(sql`

View file

@ -103,6 +103,31 @@ describe('user role routes', () => {
expect(deleteUsersRolesByUserIdAndRoleId).not.toHaveBeenCalled(); expect(deleteUsersRolesByUserIdAndRoleId).not.toHaveBeenCalled();
expect(insertUsersRoles).toHaveBeenCalledWith([]); expect(insertUsersRoles).toHaveBeenCalledWith([]);
}); });
it('POST /users/:id/roles', async () => {
findUsersRolesByUserId.mockResolvedValueOnce([]);
const response = await roleRequester.post(`/users/${mockUser.id}/roles`).send({
roleIds: [mockAdminUserRole.id],
});
expect(response.status).toEqual(201);
expect(insertUsersRoles).toHaveBeenCalledWith([
{ id: mockId, userId: mockUser.id, roleId: mockAdminUserRole.id },
]);
});
it('PUT /users/:id/roles', async () => {
findUsersRolesByUserId.mockResolvedValueOnce([mockUserRole]);
const response = await roleRequester.put(`/users/${mockUser.id}/roles`).send({
roleIds: [mockAdminUserRole2.id],
});
expect(response.status).toEqual(200);
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(
mockUser.id,
mockAdminUserRole.id
);
expect(insertUsersRoles).toHaveBeenCalledWith([
{ id: mockId, userId: mockUser.id, roleId: mockAdminUserRole2.id },
]);
}); });
it('DELETE /users/:id/roles/:roleId', async () => { it('DELETE /users/:id/roles/:roleId', async () => {
@ -116,3 +141,4 @@ describe('user role routes', () => {
); );
}); });
}); });
});

View file

@ -0,0 +1,87 @@
import { pickDefault } from '@logto/shared/esm';
import { mockAdminApplicationRole, mockApplication } from '#src/__mocks__/index.js';
import { mockId, mockStandardId } from '#src/test-utils/nanoid.js';
import { createMockQuotaLibrary } from '#src/test-utils/quota.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
await mockStandardId();
const roles = {
findRoleById: jest.fn(),
};
const { findRoleById } = roles;
const applications = {
findApplicationById: jest.fn(),
countM2mApplicationsByIds: jest.fn(async () => ({ count: 1 })),
findM2mApplicationsByIds: jest.fn(async () => [mockApplication]),
};
const applicationsRoles = {
findFirstApplicationsRolesByRoleIdAndApplicationIds: jest.fn(),
findApplicationsRolesByRoleId: jest.fn(),
insertApplicationsRoles: jest.fn(),
deleteApplicationRole: jest.fn(),
};
const {
findFirstApplicationsRolesByRoleIdAndApplicationIds,
findApplicationsRolesByRoleId,
insertApplicationsRoles,
deleteApplicationRole,
} = applicationsRoles;
const roleUserRoutes = await pickDefault(import('./role.application.js'));
const tenantContext = new MockTenant(
undefined,
{
applicationsRoles,
applications,
roles,
},
undefined,
{ quota: createMockQuotaLibrary() }
);
describe('role application routes', () => {
const roleUserRequester = createRequester({ authedRoutes: roleUserRoutes, tenantContext });
it('GET /roles/:id/applications', async () => {
findRoleById.mockResolvedValueOnce(mockAdminApplicationRole);
findApplicationsRolesByRoleId.mockResolvedValueOnce([]);
const response = await roleUserRequester.get(
`/roles/${mockAdminApplicationRole.id}/applications`
);
expect(response.status).toEqual(200);
expect(response.body[0]).toHaveProperty('id', mockApplication.id);
});
it('POST /roles/:id/applications', async () => {
findRoleById.mockResolvedValueOnce(mockAdminApplicationRole);
findFirstApplicationsRolesByRoleIdAndApplicationIds.mockResolvedValueOnce(null);
const response = await roleUserRequester
.post(`/roles/${mockAdminApplicationRole.id}/applications`)
.send({
applicationIds: [mockApplication.id],
});
expect(response.status).toEqual(201);
expect(insertApplicationsRoles).toHaveBeenCalledWith([
{ id: mockId, applicationId: mockApplication.id, roleId: mockAdminApplicationRole.id },
]);
});
it('DELETE /roles/:id/applications/:applicationId', async () => {
const response = await roleUserRequester.delete(
`/roles/${mockAdminApplicationRole.id}/applications/${mockApplication.id}`
);
expect(response.status).toEqual(204);
expect(deleteApplicationRole).toHaveBeenCalledWith(
mockApplication.id,
mockAdminApplicationRole.id
);
});
});

View file

@ -0,0 +1,132 @@
import { Applications } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { tryThat } from '@silverhand/essentials';
import { object, string } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function roleApplicationRoutes<T extends AuthedRouter>(
...[router, { queries }]: RouterInitArgs<T>
) {
const {
roles: { findRoleById },
applications: { countM2mApplicationsByIds, findM2mApplicationsByIds, findApplicationById },
applicationsRoles: {
findFirstApplicationsRolesByRoleIdAndApplicationIds,
findApplicationsRolesByRoleId,
insertApplicationsRoles,
deleteApplicationRole,
},
} = queries;
router.get(
'/roles/:id/applications',
koaPagination(),
koaGuard({
params: object({ id: string().min(1) }),
response: Applications.guard.array(),
status: [200, 204, 400, 404],
}),
async (ctx, next) => {
const {
params: { id },
} = ctx.guard;
const { limit, offset } = ctx.pagination;
const { searchParams } = ctx.request.URL;
await findRoleById(id);
return tryThat(
async () => {
const search = parseSearchParamsForSearch(searchParams);
const applicationRoles = await findApplicationsRolesByRoleId(id);
const applicationIds = applicationRoles.map(({ applicationId }) => applicationId);
const [{ count }, applications] = await Promise.all([
countM2mApplicationsByIds(search, applicationIds),
findM2mApplicationsByIds(search, limit, offset, applicationIds),
]);
ctx.pagination.totalCount = count;
ctx.body = applications;
return next();
},
(error) => {
if (error instanceof TypeError) {
throw new RequestError(
{ code: 'request.invalid_input', details: error.message },
error
);
}
throw error;
}
);
}
);
router.post(
'/roles/:id/applications',
koaGuard({
params: object({ id: string().min(1) }),
body: object({ applicationIds: string().min(1).array().nonempty() }),
status: [201, 404, 422],
}),
async (ctx, next) => {
const {
params: { id },
body: { applicationIds },
} = ctx.guard;
await findRoleById(id);
const existingRecord = await findFirstApplicationsRolesByRoleIdAndApplicationIds(
id,
applicationIds
);
if (existingRecord) {
throw new RequestError({
code: 'role.application_exists',
status: 422,
userId: existingRecord.applicationId,
});
}
await Promise.all(
applicationIds.map(async (applicationId) => findApplicationById(applicationId))
);
await insertApplicationsRoles(
applicationIds.map((applicationId) => ({
id: generateStandardId(),
roleId: id,
applicationId,
}))
);
ctx.status = 201;
return next();
}
);
router.delete(
'/roles/:id/applications/:applicationId',
koaGuard({
params: object({ id: string().min(1), applicationId: string().min(1) }),
status: [204, 404],
}),
async (ctx, next) => {
const {
params: { id, applicationId },
} = ctx.guard;
await deleteApplicationRole(applicationId, id);
ctx.status = 204;
return next();
}
);
}

View file

@ -1,9 +1,8 @@
import type { Role } from '@logto/schemas'; import type { Role } from '@logto/schemas';
import { RoleType } from '@logto/schemas';
import { pickDefault } from '@logto/shared/esm'; import { pickDefault } from '@logto/shared/esm';
import { mockAdminUserRole, mockScope, mockUser, mockResource } from '#src/__mocks__/index.js'; import { mockAdminUserRole, mockScope, mockUser } from '#src/__mocks__/index.js';
import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; import { mockStandardId } from '#src/test-utils/nanoid.js';
import { createMockQuotaLibrary } from '#src/test-utils/quota.js'; import { createMockQuotaLibrary } from '#src/test-utils/quota.js';
import { MockTenant } from '#src/test-utils/tenant.js'; import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js'; import { createRequester } from '#src/utils/test-utils.js';
@ -30,7 +29,6 @@ const roles = {
...data, ...data,
tenantId: 'fake_tenant', tenantId: 'fake_tenant',
})), })),
findRolesByRoleIds: jest.fn(),
}; };
const { findRoleByRoleName, findRoleById, deleteRoleById } = roles; const { findRoleByRoleName, findRoleById, deleteRoleById } = roles;
@ -39,36 +37,22 @@ const scopes = {
}; };
const { findScopeById } = scopes; const { findScopeById } = scopes;
const resources = {
findResourcesByIds: jest.fn(async () => [mockResource]),
};
const rolesScopes = { const rolesScopes = {
insertRolesScopes: jest.fn(), insertRolesScopes: jest.fn(),
deleteRolesScope: jest.fn(),
}; };
const { insertRolesScopes } = rolesScopes; const { insertRolesScopes } = rolesScopes;
const users = { const users = {
findUsersByIds: jest.fn(), findUsersByIds: jest.fn(),
findUserById: jest.fn(),
countUsers: jest.fn(async () => ({ count: 1 })),
findUsers: jest.fn(async () => [mockUser]),
}; };
const { findUsersByIds } = users; const { findUsersByIds } = users;
const usersRoles = { const usersRoles = {
insertUsersRoles: jest.fn(),
countUsersRolesByRoleId: jest.fn(), countUsersRolesByRoleId: jest.fn(),
findUsersRolesByRoleId: jest.fn(), findUsersRolesByRoleId: jest.fn(),
deleteUsersRolesByUserIdAndRoleId: jest.fn(), deleteUsersRolesByUserIdAndRoleId: jest.fn(),
}; };
const { const { findUsersRolesByRoleId, countUsersRolesByRoleId } = usersRoles;
insertUsersRoles,
findUsersRolesByRoleId,
deleteUsersRolesByUserIdAndRoleId,
countUsersRolesByRoleId,
} = usersRoles;
const roleRoutes = await pickDefault(import('./role.js')); const roleRoutes = await pickDefault(import('./role.js'));
@ -78,7 +62,6 @@ const tenantContext = new MockTenant(
usersRoles, usersRoles,
users, users,
rolesScopes, rolesScopes,
resources,
scopes, scopes,
roles, roles,
}, },
@ -167,49 +150,4 @@ describe('role routes', () => {
expect(response.status).toEqual(204); expect(response.status).toEqual(204);
expect(deleteRoleById).toHaveBeenCalledWith(mockAdminUserRole.id); expect(deleteRoleById).toHaveBeenCalledWith(mockAdminUserRole.id);
}); });
it('GET /roles/:id/users', async () => {
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
findUsersRolesByRoleId.mockResolvedValueOnce([]);
findUsersByIds.mockResolvedValueOnce([mockUser]);
const response = await roleRequester.get(`/roles/${mockAdminUserRole.id}/users`);
expect(response.status).toEqual(200);
expect(response.body[0]).toHaveProperty('id', mockUser.id);
});
describe('POST /roles/:id/users', () => {
it('Succeed', async () => {
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
findUsersRolesByRoleId.mockResolvedValueOnce([]);
const response = await roleRequester.post(`/roles/${mockAdminUserRole.id}/users`).send({
userIds: [mockUser.id],
});
expect(response.status).toEqual(201);
expect(insertUsersRoles).toHaveBeenCalledWith([
{ id: mockId, userId: mockUser.id, roleId: mockAdminUserRole.id },
]);
});
it('Should throw error when trying to assign machine to machine role to users', async () => {
const mockMachineToMachineRole = { ...mockAdminUserRole, type: RoleType.MachineToMachine };
findRoleById.mockResolvedValueOnce(mockMachineToMachineRole);
const response = await roleRequester
.post(`/roles/${mockMachineToMachineRole.id}/users`)
.send({
userIds: [mockUser.id],
});
expect(response.status).toEqual(422);
});
});
it('DELETE /roles/:id/users/:userId', async () => {
const response = await roleRequester.delete(
`/roles/${mockAdminUserRole.id}/users/${mockUser.id}`
);
expect(response.status).toEqual(204);
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(
mockUser.id,
mockAdminUserRole.id
);
});
}); });

View file

@ -1,13 +1,7 @@
import type { RoleResponse } from '@logto/schemas'; import type { RoleResponse } from '@logto/schemas';
import { import { Roles, Users } from '@logto/schemas';
userInfoSelectFields,
userProfileResponseGuard,
Roles,
RoleType,
Users,
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared'; import { generateStandardId } from '@logto/shared';
import { pick, tryThat } from '@silverhand/essentials'; import { tryThat } from '@silverhand/essentials';
import { object, string, z, number } from 'zod'; import { object, string, z, number } from 'zod';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
@ -18,17 +12,15 @@ import koaRoleRlsErrorHandler from '#src/middleware/koa-role-rls-error-handler.j
import assertThat from '#src/utils/assert-that.js'; import assertThat from '#src/utils/assert-that.js';
import { parseSearchParamsForSearch } from '#src/utils/search.js'; import { parseSearchParamsForSearch } from '#src/utils/search.js';
import roleApplicationRoutes from './role.application.js';
import roleUserRoutes from './role.user.js';
import type { AuthedRouter, RouterInitArgs } from './types.js'; import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function roleRoutes<T extends AuthedRouter>( export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]: RouterInitArgs<T>) {
...[ const {
router,
{
queries, queries,
libraries: { quota }, libraries: { quota },
}, } = tenant;
]: RouterInitArgs<T>
) {
const { const {
rolesScopes: { insertRolesScopes }, rolesScopes: { insertRolesScopes },
roles: { roles: {
@ -41,14 +33,8 @@ export default function roleRoutes<T extends AuthedRouter>(
updateRoleById, updateRoleById,
}, },
scopes: { findScopeById }, scopes: { findScopeById },
users: { findUserById, findUsersByIds, countUsers, findUsers }, users: { findUsersByIds },
usersRoles: { usersRoles: { countUsersRolesByRoleId, findUsersRolesByRoleId, findUsersRolesByUserId },
countUsersRolesByRoleId,
deleteUsersRolesByUserIdAndRoleId,
findUsersRolesByRoleId,
findUsersRolesByUserId,
insertUsersRoles,
},
} = queries; } = queries;
router.use('/roles(/.*)?', koaRoleRlsErrorHandler()); router.use('/roles(/.*)?', koaRoleRlsErrorHandler());
@ -228,101 +214,6 @@ export default function roleRoutes<T extends AuthedRouter>(
} }
); );
router.get( roleUserRoutes(router, tenant);
'/roles/:id/users', roleApplicationRoutes(router, tenant);
koaPagination(),
koaGuard({
params: object({ id: string().min(1) }),
response: userProfileResponseGuard.array(),
status: [200, 400, 404],
}),
async (ctx, next) => {
const {
params: { id },
} = ctx.guard;
const { limit, offset } = ctx.pagination;
const { searchParams } = ctx.request.URL;
await findRoleById(id);
return tryThat(
async () => {
const search = parseSearchParamsForSearch(searchParams);
const usersRoles = await findUsersRolesByRoleId(id);
const userIds = usersRoles.map(({ userId }) => userId);
const [{ count }, users] = await Promise.all([
countUsers(search, undefined, userIds),
findUsers(limit, offset, search, undefined, userIds),
]);
ctx.pagination.totalCount = count;
ctx.body = users.map((user) => pick(user, ...userInfoSelectFields));
return next();
},
(error) => {
if (error instanceof TypeError) {
throw new RequestError(
{ code: 'request.invalid_input', details: error.message },
error
);
}
throw error;
}
);
}
);
router.post(
'/roles/:id/users',
koaGuard({
params: object({ id: string().min(1) }),
body: object({ userIds: string().min(1).array().nonempty() }),
status: [201, 404, 422],
}),
async (ctx, next) => {
const {
params: { id },
body: { userIds },
} = ctx.guard;
const role = await findRoleById(id);
assertThat(
role.type === RoleType.User,
new RequestError({ code: 'user.invalid_role_type', status: 422, roleId: role.id })
);
const usersRoles = await findUsersRolesByRoleId(id);
const existingUserIds = new Set(usersRoles.map(({ userId }) => userId));
const userIdsToAdd = userIds.filter((id) => !existingUserIds.has(id)); // Skip existing user ids.
if (userIdsToAdd.length > 0) {
await Promise.all(userIdsToAdd.map(async (userId) => findUserById(userId)));
await insertUsersRoles(
userIdsToAdd.map((userId) => ({ id: generateStandardId(), roleId: id, userId }))
);
}
ctx.status = 201;
return next();
}
);
router.delete(
'/roles/:id/users/:userId',
koaGuard({
params: object({ id: string().min(1), userId: string().min(1) }),
status: [204, 404],
}),
async (ctx, next) => {
const {
params: { id, userId },
} = ctx.guard;
await deleteUsersRolesByUserIdAndRoleId(userId, id);
ctx.status = 204;
return next();
}
);
} }

View file

@ -0,0 +1,83 @@
import { pickDefault } from '@logto/shared/esm';
import { mockAdminUserRole, mockUser } from '#src/__mocks__/index.js';
import { mockId, mockStandardId } from '#src/test-utils/nanoid.js';
import { createMockQuotaLibrary } from '#src/test-utils/quota.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
await mockStandardId();
const roles = {
findRoleById: jest.fn(),
};
const { findRoleById } = roles;
const users = {
findUserById: jest.fn(),
countUsers: jest.fn(async () => ({ count: 1 })),
findUsers: jest.fn(async () => [mockUser]),
};
const usersRoles = {
insertUsersRoles: jest.fn(),
findUsersRolesByRoleId: jest.fn(),
findFirstUsersRolesByRoleIdAndUserIds: jest.fn(),
deleteUsersRolesByUserIdAndRoleId: jest.fn(),
};
const {
insertUsersRoles,
findUsersRolesByRoleId,
deleteUsersRolesByUserIdAndRoleId,
findFirstUsersRolesByRoleIdAndUserIds,
} = usersRoles;
const roleUserRoutes = await pickDefault(import('./role.user.js'));
const tenantContext = new MockTenant(
undefined,
{
usersRoles,
users,
roles,
},
undefined,
{ quota: createMockQuotaLibrary() }
);
describe('role user routes', () => {
const roleUserRequester = createRequester({ authedRoutes: roleUserRoutes, tenantContext });
it('GET /roles/:id/users', async () => {
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
findUsersRolesByRoleId.mockResolvedValueOnce([]);
const response = await roleUserRequester.get(`/roles/${mockAdminUserRole.id}/users`);
expect(response.status).toEqual(200);
expect(response.body[0]).toHaveProperty('id', mockUser.id);
});
it('POST /roles/:id/users', async () => {
findRoleById.mockResolvedValueOnce(mockAdminUserRole);
findFirstUsersRolesByRoleIdAndUserIds.mockResolvedValueOnce(null);
const response = await roleUserRequester.post(`/roles/${mockAdminUserRole.id}/users`).send({
userIds: [mockUser.id],
});
expect(response.status).toEqual(201);
expect(insertUsersRoles).toHaveBeenCalledWith([
{ id: mockId, userId: mockUser.id, roleId: mockAdminUserRole.id },
]);
});
it('DELETE /roles/:id/users/:userId', async () => {
const response = await roleUserRequester.delete(
`/roles/${mockAdminUserRole.id}/users/${mockUser.id}`
);
expect(response.status).toEqual(204);
expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(
mockUser.id,
mockAdminUserRole.id
);
});
});

View file

@ -0,0 +1,123 @@
import { userInfoSelectFields, userProfileResponseGuard } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { pick, tryThat } from '@silverhand/essentials';
import { object, string } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function roleUserRoutes<T extends AuthedRouter>(
...[router, { queries }]: RouterInitArgs<T>
) {
const {
roles: { findRoleById },
users: { findUserById, countUsers, findUsers },
usersRoles: {
deleteUsersRolesByUserIdAndRoleId,
findFirstUsersRolesByRoleIdAndUserIds,
findUsersRolesByRoleId,
insertUsersRoles,
},
} = queries;
router.get(
'/roles/:id/users',
koaPagination(),
koaGuard({
params: object({ id: string().min(1) }),
response: userProfileResponseGuard.array(),
status: [200, 400, 404],
}),
async (ctx, next) => {
const {
params: { id },
} = ctx.guard;
const { limit, offset } = ctx.pagination;
const { searchParams } = ctx.request.URL;
await findRoleById(id);
return tryThat(
async () => {
const search = parseSearchParamsForSearch(searchParams);
const usersRoles = await findUsersRolesByRoleId(id);
const userIds = usersRoles.map(({ userId }) => userId);
const [{ count }, users] = await Promise.all([
countUsers(search, undefined, userIds),
findUsers(limit, offset, search, undefined, userIds),
]);
ctx.pagination.totalCount = count;
ctx.body = users.map((user) => pick(user, ...userInfoSelectFields));
return next();
},
(error) => {
if (error instanceof TypeError) {
throw new RequestError(
{ code: 'request.invalid_input', details: error.message },
error
);
}
throw error;
}
);
}
);
router.post(
'/roles/:id/users',
koaGuard({
params: object({ id: string().min(1) }),
body: object({ userIds: string().min(1).array().nonempty() }),
status: [201, 404, 422],
}),
async (ctx, next) => {
const {
params: { id },
body: { userIds },
} = ctx.guard;
await findRoleById(id);
const existingRecord = await findFirstUsersRolesByRoleIdAndUserIds(id, userIds);
if (existingRecord) {
throw new RequestError({
code: 'role.user_exists',
status: 422,
userId: existingRecord.userId,
});
}
await Promise.all(userIds.map(async (userId) => findUserById(userId)));
await insertUsersRoles(
userIds.map((userId) => ({ id: generateStandardId(), roleId: id, userId }))
);
ctx.status = 201;
return next();
}
);
router.delete(
'/roles/:id/users/:userId',
koaGuard({
params: object({ id: string().min(1), userId: string().min(1) }),
status: [204, 404],
}),
async (ctx, next) => {
const {
params: { id, userId },
} = ctx.guard;
await deleteUsersRolesByUserIdAndRoleId(userId, id);
ctx.status = 204;
return next();
}
);
}

View file

@ -1,4 +1,4 @@
import type { CreateRole, Role, Scope, User } from '@logto/schemas'; import type { CreateRole, Role, Scope, User, Application } from '@logto/schemas';
import { RoleType } from '@logto/schemas'; import { RoleType } from '@logto/schemas';
import { generateRoleName } from '#src/utils.js'; import { generateRoleName } from '#src/utils.js';
@ -65,3 +65,14 @@ export const assignUsersToRole = async (userIds: string[], roleId: string) =>
export const deleteUserFromRole = async (userId: string, roleId: string) => export const deleteUserFromRole = async (userId: string, roleId: string) =>
authedAdminApi.delete(`roles/${roleId}/users/${userId}`); authedAdminApi.delete(`roles/${roleId}/users/${userId}`);
export const getRoleApplications = async (roleId: string) =>
authedAdminApi.get(`roles/${roleId}/applications`).json<Application[]>();
export const assignApplicationsToRole = async (applicationIds: string[], roleId: string) =>
authedAdminApi.post(`roles/${roleId}/applications`, {
json: { applicationIds },
});
export const deleteApplicationFromRole = async (applicationId: string, roleId: string) =>
authedAdminApi.delete(`roles/${roleId}/applications/${applicationId}`);

View file

@ -0,0 +1,95 @@
import { ApplicationType, RoleType } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { HTTPError } from 'got';
import { createApplication } from '#src/api/index.js';
import {
assignApplicationsToRole,
createRole,
deleteApplicationFromRole,
getRoleApplications,
} from '#src/api/role.js';
describe('roles applications', () => {
it('should get role applications successfully', async () => {
const role = await createRole({ type: RoleType.MachineToMachine });
const m2mApp = await createApplication(generateStandardId(), ApplicationType.MachineToMachine);
await assignApplicationsToRole([m2mApp.id], role.id);
const applications = await getRoleApplications(role.id);
expect(applications.length).toBe(1);
expect(applications[0]).toHaveProperty('id', m2mApp.id);
});
it('should return 404 if role not found', async () => {
const response = await getRoleApplications('not-found').catch((error: unknown) => error);
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
});
it('should assign applications to role successfully', async () => {
const role = await createRole({ type: RoleType.MachineToMachine });
const m2mApp1 = await createApplication(generateStandardId(), ApplicationType.MachineToMachine);
const m2mApp2 = await createApplication(generateStandardId(), ApplicationType.MachineToMachine);
await assignApplicationsToRole([m2mApp1.id, m2mApp2.id], role.id);
const applications = await getRoleApplications(role.id);
expect(applications.length).toBe(2);
});
it('should fail when try to assign empty applications', async () => {
const role = await createRole({ type: RoleType.MachineToMachine });
const response = await assignApplicationsToRole([], role.id).catch((error: unknown) => error);
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
});
it('should fail with invalid application input', async () => {
const role = await createRole({ type: RoleType.MachineToMachine });
const response = await assignApplicationsToRole([''], role.id).catch((error: unknown) => error);
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
});
it('should fail if role not found', async () => {
const m2mApp = await createApplication(generateStandardId(), ApplicationType.MachineToMachine);
const response = await assignApplicationsToRole([m2mApp.id], 'not-found').catch(
(error: unknown) => error
);
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
});
it('should fail if application not found', async () => {
const role = await createRole({ type: RoleType.MachineToMachine });
const response = await assignApplicationsToRole(['not-found'], role.id).catch(
(error: unknown) => error
);
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
});
it('should remove application from role successfully', async () => {
const role = await createRole({ type: RoleType.MachineToMachine });
const m2mApp = await createApplication(generateStandardId(), ApplicationType.MachineToMachine);
await assignApplicationsToRole([m2mApp.id], role.id);
const applications = await getRoleApplications(role.id);
expect(applications.length).toBe(1);
await deleteApplicationFromRole(m2mApp.id, role.id);
const newApplications = await getRoleApplications(role.id);
expect(newApplications.length).toBe(0);
});
it('should fail if role not found when trying to remove application from role', async () => {
const m2mApp = await createApplication(generateStandardId(), ApplicationType.MachineToMachine);
const response = await deleteApplicationFromRole(m2mApp.id, 'not-found').catch(
(error: unknown) => error
);
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
});
it('should fail if application not found when trying to remove application from role', async () => {
const role = await createRole({ type: RoleType.MachineToMachine });
const response = await deleteApplicationFromRole('not-found', role.id).catch(
(error: unknown) => error
);
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
});
});

View file

@ -36,7 +36,7 @@ describe('roles users', () => {
const m2mRole = await createRole({ type: RoleType.MachineToMachine }); const m2mRole = await createRole({ type: RoleType.MachineToMachine });
const user = await createUser(generateNewUserProfile({})); const user = await createUser(generateNewUserProfile({}));
await expectRejects(assignUsersToRole([user.id], m2mRole.id), { await expectRejects(assignUsersToRole([user.id], m2mRole.id), {
code: 'user.invalid_role_type', code: 'entity.db_constraint_violated',
statusCode: 422, statusCode: 422,
}); });
const users = await getRoleUsers(m2mRole.id); const users = await getRoleUsers(m2mRole.id);

View file

@ -2,6 +2,8 @@ const role = {
name_in_use: 'Dieser Rollenname {{name}} wird bereits verwendet.', name_in_use: 'Dieser Rollenname {{name}} wird bereits verwendet.',
scope_exists: 'Die Scope-ID {{scopeId}} wurde bereits zu dieser Rolle hinzugefügt.', scope_exists: 'Die Scope-ID {{scopeId}} wurde bereits zu dieser Rolle hinzugefügt.',
user_exists: 'Die Benutzer-ID {{userId}} wurde bereits zu dieser Rolle hinzugefügt.', user_exists: 'Die Benutzer-ID {{userId}} wurde bereits zu dieser Rolle hinzugefügt.',
application_exists:
'Die Anwendungs-ID {{applicationId}} wurde bereits zu dieser Rolle hinzugefügt.',
default_role_missing: default_role_missing:
'Einige der Standardrollennamen sind in der Datenbank nicht vorhanden. Bitte stellen Sie sicher, dass Sie zuerst Rollen erstellen.', 'Einige der Standardrollennamen sind in der Datenbank nicht vorhanden. Bitte stellen Sie sicher, dass Sie zuerst Rollen erstellen.',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'This role name {{name}} is already in use', name_in_use: 'This role name {{name}} is already in use',
scope_exists: 'The scope id {{scopeId}} has already been added to this role', scope_exists: 'The scope id {{scopeId}} has already been added to this role',
user_exists: 'The user id {{userId}} is already been added to this role', user_exists: 'The user id {{userId}} is already been added to this role',
application_exists: 'The application id {{applicationId}} is already been added to this role',
default_role_missing: default_role_missing:
'Some of the default roleNames does not exist in database, please ensure to create roles first', 'Some of the default roleNames does not exist in database, please ensure to create roles first',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'Este nombre de rol {{name}} ya está en uso', name_in_use: 'Este nombre de rol {{name}} ya está en uso',
scope_exists: 'El id de alcance {{scopeId}} ya ha sido agregado a este rol', scope_exists: 'El id de alcance {{scopeId}} ya ha sido agregado a este rol',
user_exists: 'El id de usuario {{userId}} ya ha sido agregado a este rol', user_exists: 'El id de usuario {{userId}} ya ha sido agregado a este rol',
application_exists: 'El id de aplicación {{applicationId}} ya ha sido agregado a este rol',
default_role_missing: default_role_missing:
'Algunos de los nombres de roles predeterminados no existen en la base de datos, por favor asegúrese de crear los roles primero', 'Algunos de los nombres de roles predeterminados no existen en la base de datos, por favor asegúrese de crear los roles primero',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'Ce nom de rôle {{name}} est déjà utilisé', name_in_use: 'Ce nom de rôle {{name}} est déjà utilisé',
scope_exists: "L'identifiant de portée {{scopeId}} a déjà été ajouté à ce rôle", scope_exists: "L'identifiant de portée {{scopeId}} a déjà été ajouté à ce rôle",
user_exists: "L'identifiant d'utilisateur {{userId}} a déjà été ajouté à ce rôle", user_exists: "L'identifiant d'utilisateur {{userId}} a déjà été ajouté à ce rôle",
application_exists: "L'identifiant d'application {{applicationId}} a déjà été ajouté à ce rôle",
default_role_missing: default_role_missing:
"Certains noms de rôles par défaut n'existent pas dans la base de données, veuillez vous assurer de créer d'abord des rôles", "Certains noms de rôles par défaut n'existent pas dans la base de données, veuillez vous assurer de créer d'abord des rôles",
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,8 @@ const role = {
name_in_use: 'Il nome di ruolo {{name}} è già in uso', name_in_use: 'Il nome di ruolo {{name}} è già in uso',
scope_exists: "L'identificatore di ambito {{scopeId}} è già stato aggiunto a questo ruolo", scope_exists: "L'identificatore di ambito {{scopeId}} è già stato aggiunto a questo ruolo",
user_exists: "L'identificatore di utente {{userId}} è già stato aggiunto a questo ruolo", user_exists: "L'identificatore di utente {{userId}} è già stato aggiunto a questo ruolo",
application_exists:
"L'ID dell'applicazione {{applicationId}} è già stato aggiunto a questo ruolo",
default_role_missing: default_role_missing:
'Alcuni dei nomi di ruolo predefiniti non esistono nel database, assicurati di creare prima i ruoli', 'Alcuni dei nomi di ruolo predefiniti non esistono nel database, assicurati di creare prima i ruoli',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'このロール名{{name}}はすでに使用されています', name_in_use: 'このロール名{{name}}はすでに使用されています',
scope_exists: 'スコープID {{scopeId}}はすでにこのロールに追加されています', scope_exists: 'スコープID {{scopeId}}はすでにこのロールに追加されています',
user_exists: 'ユーザーID{{userId}}はすでにこのロールに追加されています', user_exists: 'ユーザーID{{userId}}はすでにこのロールに追加されています',
application_exists: 'アプリケーション ID {{applicationId}} はすでにこのロールに追加されています',
default_role_missing: default_role_missing:
'データベースにデフォルトロール名が存在しないものがあります。ロールを作成してください', 'データベースにデフォルトロール名が存在しないものがあります。ロールを作成してください',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: '역할 이름 {{name}}이/가 이미 사용 중이에요.', name_in_use: '역할 이름 {{name}}이/가 이미 사용 중이에요.',
scope_exists: '범위 ID {{scopeId}}이/가 이미 이 역할에 추가되어 있어요.', scope_exists: '범위 ID {{scopeId}}이/가 이미 이 역할에 추가되어 있어요.',
user_exists: '사용자 ID {{userId}}이/가 이미 이 역할에 추가되어 있어요.', user_exists: '사용자 ID {{userId}}이/가 이미 이 역할에 추가되어 있어요.',
application_exists: '애플리케이션 ID {{applicationId}} 가 이미 이 역할에 추가되어 있어요.',
default_role_missing: default_role_missing:
'기본 역할 이름의 일부가 데이터베이스에 존재하지 않아요. 먼저 역할을 생성해 주세요.', '기본 역할 이름의 일부가 데이터베이스에 존재하지 않아요. 먼저 역할을 생성해 주세요.',
internal_role_violation: internal_role_violation:

View file

@ -2,10 +2,11 @@ const role = {
name_in_use: 'Ta nazwa roli {{name}} jest już w użyciu', name_in_use: 'Ta nazwa roli {{name}} jest już w użyciu',
scope_exists: 'Identyfikator zakresu {{scopeId}} został już dodany do tej roli', scope_exists: 'Identyfikator zakresu {{scopeId}} został już dodany do tej roli',
user_exists: 'Identyfikator użytkownika {{userId}} został już dodany do tej roli', user_exists: 'Identyfikator użytkownika {{userId}} został już dodany do tej roli',
application_exists: 'Identyfikator aplikacji {{applicationId}} został już dodany do tej roli',
default_role_missing: default_role_missing:
'Niektóre z domyślnych nazw ról nie istnieją w bazie danych, upewnij się, że najpierw utworzysz role', 'Niektóre z domyślnych nazw ról nie istnieją w bazie danych, upewnij się, że najpierw utworzysz role',
internal_role_violation: internal_role_violation:
'Możesz próbować zaktualizować lub usunąć rolę wewnętrzną, co jest zabronione przez Logto. Jeśli tworzysz nową rolę, spróbuj innej nazwy, która nie zaczyna się od "#internal:".', 'Możesz próbować zaktualizować lub usunąć rolę wewnętrzną, co jest zabronione przez Logto. Jeśli tworzysz nową rolę, spróbuj innej nazwy, która nie zaczyna się od "#internal:".',
}; };
export default Object.freeze(role); export default Object.freeze(role);

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'Este nome de papel {{name}} já está em uso', name_in_use: 'Este nome de papel {{name}} já está em uso',
scope_exists: 'O id de escopo {{scopeId}} já foi adicionado a este papel', scope_exists: 'O id de escopo {{scopeId}} já foi adicionado a este papel',
user_exists: 'O id de usuário {{userId}} já foi adicionado a este papel', user_exists: 'O id de usuário {{userId}} já foi adicionado a este papel',
application_exists: 'O id do aplicativo {{applicationId}} já foi adicionado a este papel',
default_role_missing: default_role_missing:
'Alguns dos nomes de função padrão não existem no banco de dados, certifique-se de criar funções primeiro', 'Alguns dos nomes de função padrão não existem no banco de dados, certifique-se de criar funções primeiro',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'Este nome de função {{name}} já está em uso', name_in_use: 'Este nome de função {{name}} já está em uso',
scope_exists: 'O id do escopo {{scopeId}} já foi adicionado a esta função', scope_exists: 'O id do escopo {{scopeId}} já foi adicionado a esta função',
user_exists: 'O id do usuário {{userId}} já foi adicionado a esta função', user_exists: 'O id do usuário {{userId}} já foi adicionado a esta função',
application_exists: 'O id de aplicação {{applicationId}} já foi adicionado a esta função',
default_role_missing: default_role_missing:
'Alguns dos nomes de função padrão não existem no banco de dados, por favor, certifique-se de criar as funções primeiro', 'Alguns dos nomes de função padrão não existem no banco de dados, por favor, certifique-se de criar as funções primeiro',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'Это имя роли {{name}} уже используется', name_in_use: 'Это имя роли {{name}} уже используется',
scope_exists: 'Идентификатор области действия {{scopeId}} уже был добавлен в эту роль', scope_exists: 'Идентификатор области действия {{scopeId}} уже был добавлен в эту роль',
user_exists: 'Идентификатор пользователя {{userId}} уже был добавлен в эту роль', user_exists: 'Идентификатор пользователя {{userId}} уже был добавлен в эту роль',
application_exists: 'Идентификатор приложения {{applicationId}} уже был добавлен в эту роль',
default_role_missing: default_role_missing:
'Некоторые имена ролей по умолчанию отсутствуют в базе данных, пожалуйста, убедитесь в том, что сначала создали роли', 'Некоторые имена ролей по умолчанию отсутствуют в базе данных, пожалуйста, убедитесь в том, что сначала создали роли',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: 'Bu rol adı {{name}} zaten kullanımda', name_in_use: 'Bu rol adı {{name}} zaten kullanımda',
scope_exists: 'Bu kapsam kimliği {{scopeId}} zaten bu role eklendi', scope_exists: 'Bu kapsam kimliği {{scopeId}} zaten bu role eklendi',
user_exists: 'Bu kullanıcı kimliği {{userId}} zaten bu role eklendi', user_exists: 'Bu kullanıcı kimliği {{userId}} zaten bu role eklendi',
application_exists: 'Bu uygulama kimliği {{applicationId}} zaten bu role eklendi',
default_role_missing: default_role_missing:
'Varsayılan rol adlarından bazıları veritabanında mevcut değil, lütfen önce rolleri oluşturduğunuzdan emin olun', 'Varsayılan rol adlarından bazıları veritabanında mevcut değil, lütfen önce rolleri oluşturduğunuzdan emin olun',
internal_role_violation: internal_role_violation:

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: '此角色名称 {{name}} 已被使用', name_in_use: '此角色名称 {{name}} 已被使用',
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色', scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
user_exists: '用户 ID {{userId}} 已添加到此角色', user_exists: '用户 ID {{userId}} 已添加到此角色',
application_exists: '应用程序 ID {{applicationId}} 已添加到此角色',
default_role_missing: '某些默认角色名称在数据库中不存在,请确保先创建角色', default_role_missing: '某些默认角色名称在数据库中不存在,请确保先创建角色',
internal_role_violation: internal_role_violation:
'你可能正在尝试更新或删除 Logto 禁止的内部角色。如果你要创建新角色,请尝试使用不以“#internal:”开头的名称。', '你可能正在尝试更新或删除 Logto 禁止的内部角色。如果你要创建新角色,请尝试使用不以“#internal:”开头的名称。',

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: '此角色名稱 {{name}} 已被使用', name_in_use: '此角色名稱 {{name}} 已被使用',
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色', scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
user_exists: '用戶 ID {{userId}} 已添加到此角色', user_exists: '用戶 ID {{userId}} 已添加到此角色',
application_exists: '應用程式 ID {{applicationId}} 已添加到此角色',
default_role_missing: '某些默認角色名稱在數據庫中不存在,請確保先創建角色', default_role_missing: '某些默認角色名稱在數據庫中不存在,請確保先創建角色',
internal_role_violation: internal_role_violation:
'你可能正在嘗試更新或刪除 Logto 禁止的內部角色。如果你要創建新角色,請嘗試使用不以“#internal:”開頭的名稱。', '你可能正在嘗試更新或刪除 Logto 禁止的內部角色。如果你要創建新角色,請嘗試使用不以“#internal:”開頭的名稱。',

View file

@ -2,6 +2,7 @@ const role = {
name_in_use: '此角色名稱 {{name}} 已被使用', name_in_use: '此角色名稱 {{name}} 已被使用',
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色', scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
user_exists: '用戶 ID {{userId}} 已添加到此角色', user_exists: '用戶 ID {{userId}} 已添加到此角色',
application_exists: '已經將應用程式 ID {{applicationId}} 添加到此角色',
default_role_missing: '某些預設角色名稱在資料庫中不存在,請確保先創建角色', default_role_missing: '某些預設角色名稱在資料庫中不存在,請確保先創建角色',
internal_role_violation: internal_role_violation:
'你可能正在嘗試更新或刪除 Logto 禁止的內部角色。如果你要創建新角色,請嘗試使用不以“#internal:”開頭的名稱。', '你可能正在嘗試更新或刪除 Logto 禁止的內部角色。如果你要創建新角色,請嘗試使用不以“#internal:”開頭的名稱。',