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:
parent
709ba633b8
commit
285aa745e7
29 changed files with 710 additions and 204 deletions
|
@ -58,6 +58,14 @@ export const mockScopeWithResource = {
|
|||
resource: mockResource,
|
||||
};
|
||||
|
||||
export const mockAdminApplicationRole: Role = {
|
||||
tenantId: 'fake_tenant',
|
||||
id: 'role_id',
|
||||
name: 'admin',
|
||||
description: 'admin application',
|
||||
type: RoleType.MachineToMachine,
|
||||
};
|
||||
|
||||
export const mockAdminUserRole: Role = {
|
||||
tenantId: 'fake_tenant',
|
||||
id: 'role_id',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { CreateApplication } from '@logto/schemas';
|
||||
import type { Application, CreateApplication } from '@logto/schemas';
|
||||
import { ApplicationType, Applications } from '@logto/schemas';
|
||||
import type { OmitAutoSetFields } from '@logto/shared';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { convertToIdentifiers, conditionalSql } from '@logto/shared';
|
||||
import type { CommonQueryMethods } 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 { buildUpdateWhereWithPool } from '#src/database/update-where.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 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) => {
|
||||
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, {
|
||||
returning: true,
|
||||
});
|
||||
|
||||
const updateApplication = buildUpdateWhereWithPool(pool)(Applications, true);
|
||||
|
||||
const updateApplicationById = async (
|
||||
id: string,
|
||||
set: Partial<OmitAutoSetFields<CreateApplication>>
|
||||
) => updateApplication({ set, where: { id }, jsonbMode: 'merge' });
|
||||
|
||||
const countNonM2mApplications = async () => {
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
|
@ -37,6 +59,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
|
||||
return { count: Number(count) };
|
||||
};
|
||||
|
||||
const countM2mApplications = async () => {
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
|
@ -47,6 +70,43 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
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 { rowCount } = await pool.query(sql`
|
||||
delete from ${table}
|
||||
|
@ -67,6 +127,8 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
updateApplicationById,
|
||||
countNonM2mApplications,
|
||||
countM2mApplications,
|
||||
countM2mApplicationsByIds,
|
||||
findM2mApplicationsByIds,
|
||||
deleteApplicationById,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { ApplicationsRole, CreateApplicationsRole, Role } from '@logto/schemas';
|
||||
import { Roles, ApplicationsRoles, RolesScopes } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { type Nullable } from '@silverhand/essentials';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
|
@ -10,6 +11,27 @@ const { table, fields } = convertToIdentifiers(ApplicationsRoles, true);
|
|||
const { fields: insertFields } = convertToIdentifiers(ApplicationsRoles);
|
||||
|
||||
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) =>
|
||||
pool.any<ApplicationsRole & { role: Role }>(sql`
|
||||
select
|
||||
|
@ -20,6 +42,14 @@ export const createApplicationsRolesQueries = (pool: CommonQueryMethods) => {
|
|||
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[]) =>
|
||||
pool.query(sql`
|
||||
insert into ${table} (${insertFields.id}, ${insertFields.applicationId}, ${
|
||||
|
@ -45,7 +75,10 @@ export const createApplicationsRolesQueries = (pool: CommonQueryMethods) => {
|
|||
};
|
||||
|
||||
return {
|
||||
findFirstApplicationsRolesByRoleIdAndApplicationIds,
|
||||
countApplicationsRolesByRoleId,
|
||||
findApplicationsRolesByApplicationId,
|
||||
findApplicationsRolesByRoleId,
|
||||
insertApplicationsRoles,
|
||||
deleteApplicationRole,
|
||||
};
|
||||
|
|
|
@ -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) => {
|
||||
const countRoles = async (
|
||||
search: Search = defaultUserSearch,
|
||||
search: Search = defaultSearch,
|
||||
{ excludeRoleIds = [], roleIds }: { excludeRoleIds?: string[]; roleIds?: string[] } = {}
|
||||
) => {
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
|
|
|
@ -103,6 +103,31 @@ describe('user role routes', () => {
|
|||
expect(deleteUsersRolesByUserIdAndRoleId).not.toHaveBeenCalled();
|
||||
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 () => {
|
||||
|
@ -116,3 +141,4 @@ describe('user role routes', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
87
packages/core/src/routes/role.application.test.ts
Normal file
87
packages/core/src/routes/role.application.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
132
packages/core/src/routes/role.application.ts
Normal file
132
packages/core/src/routes/role.application.ts
Normal 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();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import type { Role } from '@logto/schemas';
|
||||
import { RoleType } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import { mockAdminUserRole, mockScope, mockUser, mockResource } from '#src/__mocks__/index.js';
|
||||
import { mockId, mockStandardId } from '#src/test-utils/nanoid.js';
|
||||
import { mockAdminUserRole, mockScope, mockUser } from '#src/__mocks__/index.js';
|
||||
import { 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';
|
||||
|
@ -30,7 +29,6 @@ const roles = {
|
|||
...data,
|
||||
tenantId: 'fake_tenant',
|
||||
})),
|
||||
findRolesByRoleIds: jest.fn(),
|
||||
};
|
||||
const { findRoleByRoleName, findRoleById, deleteRoleById } = roles;
|
||||
|
||||
|
@ -39,36 +37,22 @@ const scopes = {
|
|||
};
|
||||
const { findScopeById } = scopes;
|
||||
|
||||
const resources = {
|
||||
findResourcesByIds: jest.fn(async () => [mockResource]),
|
||||
};
|
||||
|
||||
const rolesScopes = {
|
||||
insertRolesScopes: jest.fn(),
|
||||
deleteRolesScope: jest.fn(),
|
||||
};
|
||||
const { insertRolesScopes } = rolesScopes;
|
||||
|
||||
const users = {
|
||||
findUsersByIds: jest.fn(),
|
||||
findUserById: jest.fn(),
|
||||
countUsers: jest.fn(async () => ({ count: 1 })),
|
||||
findUsers: jest.fn(async () => [mockUser]),
|
||||
};
|
||||
const { findUsersByIds } = users;
|
||||
|
||||
const usersRoles = {
|
||||
insertUsersRoles: jest.fn(),
|
||||
countUsersRolesByRoleId: jest.fn(),
|
||||
findUsersRolesByRoleId: jest.fn(),
|
||||
deleteUsersRolesByUserIdAndRoleId: jest.fn(),
|
||||
};
|
||||
const {
|
||||
insertUsersRoles,
|
||||
findUsersRolesByRoleId,
|
||||
deleteUsersRolesByUserIdAndRoleId,
|
||||
countUsersRolesByRoleId,
|
||||
} = usersRoles;
|
||||
const { findUsersRolesByRoleId, countUsersRolesByRoleId } = usersRoles;
|
||||
|
||||
const roleRoutes = await pickDefault(import('./role.js'));
|
||||
|
||||
|
@ -78,7 +62,6 @@ const tenantContext = new MockTenant(
|
|||
usersRoles,
|
||||
users,
|
||||
rolesScopes,
|
||||
resources,
|
||||
scopes,
|
||||
roles,
|
||||
},
|
||||
|
@ -167,49 +150,4 @@ describe('role routes', () => {
|
|||
expect(response.status).toEqual(204);
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import type { RoleResponse } from '@logto/schemas';
|
||||
import {
|
||||
userInfoSelectFields,
|
||||
userProfileResponseGuard,
|
||||
Roles,
|
||||
RoleType,
|
||||
Users,
|
||||
} from '@logto/schemas';
|
||||
import { Roles, Users } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { pick, tryThat } from '@silverhand/essentials';
|
||||
import { tryThat } from '@silverhand/essentials';
|
||||
import { object, string, z, number } from 'zod';
|
||||
|
||||
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 { 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';
|
||||
|
||||
export default function roleRoutes<T extends AuthedRouter>(
|
||||
...[
|
||||
router,
|
||||
{
|
||||
export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]: RouterInitArgs<T>) {
|
||||
const {
|
||||
queries,
|
||||
libraries: { quota },
|
||||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
} = tenant;
|
||||
const {
|
||||
rolesScopes: { insertRolesScopes },
|
||||
roles: {
|
||||
|
@ -41,14 +33,8 @@ export default function roleRoutes<T extends AuthedRouter>(
|
|||
updateRoleById,
|
||||
},
|
||||
scopes: { findScopeById },
|
||||
users: { findUserById, findUsersByIds, countUsers, findUsers },
|
||||
usersRoles: {
|
||||
countUsersRolesByRoleId,
|
||||
deleteUsersRolesByUserIdAndRoleId,
|
||||
findUsersRolesByRoleId,
|
||||
findUsersRolesByUserId,
|
||||
insertUsersRoles,
|
||||
},
|
||||
users: { findUsersByIds },
|
||||
usersRoles: { countUsersRolesByRoleId, findUsersRolesByRoleId, findUsersRolesByUserId },
|
||||
} = queries;
|
||||
|
||||
router.use('/roles(/.*)?', koaRoleRlsErrorHandler());
|
||||
|
@ -228,101 +214,6 @@ export default function roleRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
);
|
||||
roleUserRoutes(router, tenant);
|
||||
roleApplicationRoutes(router, tenant);
|
||||
}
|
||||
|
|
83
packages/core/src/routes/role.user.test.ts
Normal file
83
packages/core/src/routes/role.user.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
123
packages/core/src/routes/role.user.ts
Normal file
123
packages/core/src/routes/role.user.ts
Normal 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();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 { 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) =>
|
||||
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}`);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -36,7 +36,7 @@ describe('roles users', () => {
|
|||
const m2mRole = await createRole({ type: RoleType.MachineToMachine });
|
||||
const user = await createUser(generateNewUserProfile({}));
|
||||
await expectRejects(assignUsersToRole([user.id], m2mRole.id), {
|
||||
code: 'user.invalid_role_type',
|
||||
code: 'entity.db_constraint_violated',
|
||||
statusCode: 422,
|
||||
});
|
||||
const users = await getRoleUsers(m2mRole.id);
|
||||
|
|
|
@ -2,6 +2,8 @@ const role = {
|
|||
name_in_use: 'Dieser Rollenname {{name}} wird bereits verwendet.',
|
||||
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.',
|
||||
application_exists:
|
||||
'Die Anwendungs-ID {{applicationId}} wurde bereits zu dieser Rolle hinzugefügt.',
|
||||
default_role_missing:
|
||||
'Einige der Standardrollennamen sind in der Datenbank nicht vorhanden. Bitte stellen Sie sicher, dass Sie zuerst Rollen erstellen.',
|
||||
internal_role_violation:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: 'This role name {{name}} is already in use',
|
||||
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',
|
||||
application_exists: 'The application id {{applicationId}} is already been added to this role',
|
||||
default_role_missing:
|
||||
'Some of the default roleNames does not exist in database, please ensure to create roles first',
|
||||
internal_role_violation:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
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',
|
||||
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:
|
||||
'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:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
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",
|
||||
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:
|
||||
"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:
|
||||
|
|
|
@ -2,6 +2,8 @@ const role = {
|
|||
name_in_use: 'Il nome di ruolo {{name}} è già in uso',
|
||||
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",
|
||||
application_exists:
|
||||
"L'ID dell'applicazione {{applicationId}} è già stato aggiunto a questo ruolo",
|
||||
default_role_missing:
|
||||
'Alcuni dei nomi di ruolo predefiniti non esistono nel database, assicurati di creare prima i ruoli',
|
||||
internal_role_violation:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: 'このロール名{{name}}はすでに使用されています',
|
||||
scope_exists: 'スコープID {{scopeId}}はすでにこのロールに追加されています',
|
||||
user_exists: 'ユーザーID{{userId}}はすでにこのロールに追加されています',
|
||||
application_exists: 'アプリケーション ID {{applicationId}} はすでにこのロールに追加されています',
|
||||
default_role_missing:
|
||||
'データベースにデフォルトロール名が存在しないものがあります。ロールを作成してください',
|
||||
internal_role_violation:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: '역할 이름 {{name}}이/가 이미 사용 중이에요.',
|
||||
scope_exists: '범위 ID {{scopeId}}이/가 이미 이 역할에 추가되어 있어요.',
|
||||
user_exists: '사용자 ID {{userId}}이/가 이미 이 역할에 추가되어 있어요.',
|
||||
application_exists: '애플리케이션 ID {{applicationId}} 가 이미 이 역할에 추가되어 있어요.',
|
||||
default_role_missing:
|
||||
'기본 역할 이름의 일부가 데이터베이스에 존재하지 않아요. 먼저 역할을 생성해 주세요.',
|
||||
internal_role_violation:
|
||||
|
|
|
@ -2,10 +2,11 @@ const role = {
|
|||
name_in_use: 'Ta nazwa roli {{name}} jest już w użyciu',
|
||||
scope_exists: 'Identyfikator zakresu {{scopeId}} 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:
|
||||
'Niektóre z domyślnych nazw ról nie istnieją w bazie danych, upewnij się, że najpierw utworzysz role',
|
||||
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);
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
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',
|
||||
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:
|
||||
'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:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
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',
|
||||
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:
|
||||
'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:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: 'Это имя роли {{name}} уже используется',
|
||||
scope_exists: 'Идентификатор области действия {{scopeId}} уже был добавлен в эту роль',
|
||||
user_exists: 'Идентификатор пользователя {{userId}} уже был добавлен в эту роль',
|
||||
application_exists: 'Идентификатор приложения {{applicationId}} уже был добавлен в эту роль',
|
||||
default_role_missing:
|
||||
'Некоторые имена ролей по умолчанию отсутствуют в базе данных, пожалуйста, убедитесь в том, что сначала создали роли',
|
||||
internal_role_violation:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: 'Bu rol adı {{name}} zaten kullanımda',
|
||||
scope_exists: 'Bu kapsam kimliği {{scopeId}} 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:
|
||||
'Varsayılan rol adlarından bazıları veritabanında mevcut değil, lütfen önce rolleri oluşturduğunuzdan emin olun',
|
||||
internal_role_violation:
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: '此角色名称 {{name}} 已被使用',
|
||||
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
|
||||
user_exists: '用户 ID {{userId}} 已添加到此角色',
|
||||
application_exists: '应用程序 ID {{applicationId}} 已添加到此角色',
|
||||
default_role_missing: '某些默认角色名称在数据库中不存在,请确保先创建角色',
|
||||
internal_role_violation:
|
||||
'你可能正在尝试更新或删除 Logto 禁止的内部角色。如果你要创建新角色,请尝试使用不以“#internal:”开头的名称。',
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: '此角色名稱 {{name}} 已被使用',
|
||||
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
|
||||
user_exists: '用戶 ID {{userId}} 已添加到此角色',
|
||||
application_exists: '應用程式 ID {{applicationId}} 已添加到此角色',
|
||||
default_role_missing: '某些默認角色名稱在數據庫中不存在,請確保先創建角色',
|
||||
internal_role_violation:
|
||||
'你可能正在嘗試更新或刪除 Logto 禁止的內部角色。如果你要創建新角色,請嘗試使用不以“#internal:”開頭的名稱。',
|
||||
|
|
|
@ -2,6 +2,7 @@ const role = {
|
|||
name_in_use: '此角色名稱 {{name}} 已被使用',
|
||||
scope_exists: '作用域 ID {{scopeId}} 已添加到此角色',
|
||||
user_exists: '用戶 ID {{userId}} 已添加到此角色',
|
||||
application_exists: '已經將應用程式 ID {{applicationId}} 添加到此角色',
|
||||
default_role_missing: '某些預設角色名稱在資料庫中不存在,請確保先創建角色',
|
||||
internal_role_violation:
|
||||
'你可能正在嘗試更新或刪除 Logto 禁止的內部角色。如果你要創建新角色,請嘗試使用不以“#internal:”開頭的名稱。',
|
||||
|
|
Loading…
Reference in a new issue