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,
|
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',
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
@ -115,4 +140,5 @@ describe('user role routes', () => {
|
||||||
mockAdminUserRole.id
|
mockAdminUserRole.id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
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 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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
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 { 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}`);
|
||||||
|
|
|
@ -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 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);
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:”开头的名称。',
|
||||||
|
|
|
@ -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:”開頭的名稱。',
|
||||||
|
|
|
@ -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:”開頭的名稱。',
|
||||||
|
|
Loading…
Reference in a new issue