diff --git a/packages/core/src/__mocks__/index.ts b/packages/core/src/__mocks__/index.ts index 96c450a55..dd6ae7c80 100644 --- a/packages/core/src/__mocks__/index.ts +++ b/packages/core/src/__mocks__/index.ts @@ -6,6 +6,7 @@ import type { Resource, Role, Scope, + UsersRole, } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas'; @@ -72,6 +73,13 @@ export const mockRole: Role = { description: 'admin', }; +export const mockRole2: Role = { + tenantId: 'fake_tenant', + id: 'role_id2', + name: 'admin2', + description: 'admin2', +}; + export const mockAdminConsoleData: AdminConsoleData = { demoChecked: false, applicationCreated: false, @@ -93,3 +101,10 @@ export const mockPasscode: Passcode = { tryCount: 2, createdAt: 10, }; + +export const mockUserRole: UsersRole = { + tenantId: 'fake_tenant', + id: 'user_role_id', + userId: 'foo', + roleId: 'role_id', +}; diff --git a/packages/core/src/routes/admin-user-role.test.ts b/packages/core/src/routes/admin-user-role.test.ts index 6122a767a..09f9f1b8a 100644 --- a/packages/core/src/routes/admin-user-role.test.ts +++ b/packages/core/src/routes/admin-user-role.test.ts @@ -1,6 +1,6 @@ import { pickDefault } from '@logto/shared/esm'; -import { mockRole, mockUser } from '#src/__mocks__/index.js'; +import { mockRole, mockUser, mockRole2, mockUserRole } from '#src/__mocks__/index.js'; import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; @@ -50,6 +50,18 @@ describe('user role routes', () => { ]); }); + it('PUT /users/:id/roles', async () => { + findUsersRolesByUserId.mockResolvedValueOnce([mockUserRole]); + const response = await roleRequester.put(`/users/${mockUser.id}/roles`).send({ + roleIds: [mockRole2.id], + }); + expect(response.status).toEqual(200); + expect(deleteUsersRolesByUserIdAndRoleId).toHaveBeenCalledWith(mockUser.id, mockRole.id); + expect(insertUsersRoles).toHaveBeenCalledWith([ + { id: mockId, userId: mockUser.id, roleId: mockRole2.id }, + ]); + }); + it('DELETE /users/:id/roles/:roleId', async () => { const response = await roleRequester.delete(`/users/${mockUser.id}/roles/${mockRole.id}`); expect(response.status).toEqual(204); diff --git a/packages/core/src/routes/admin-user-role.ts b/packages/core/src/routes/admin-user-role.ts index bc126fd91..53beaa1b6 100644 --- a/packages/core/src/routes/admin-user-role.ts +++ b/packages/core/src/routes/admin-user-role.ts @@ -99,6 +99,44 @@ export default function adminUserRoleRoutes( } ); + router.put( + '/users/:userId/roles', + koaGuard({ + params: object({ userId: string() }), + body: object({ roleIds: string().min(1).array() }), + }), + async (ctx, next) => { + const { + params: { userId }, + body: { roleIds }, + } = ctx.guard; + + await findUserById(userId); + const usersRoles = await findUsersRolesByUserId(userId); + + // Only add the ones that doesn't exist + const roleIdsToAdd = roleIds.filter( + (roleId) => !usersRoles.some(({ roleId: _roleId }) => _roleId === roleId) + ); + // Remove existing roles that isn't wanted by user anymore + const roleIdsToRemove = usersRoles + .filter(({ roleId }) => !roleIds.includes(roleId)) + .map(({ roleId }) => roleId); + + await Promise.all(roleIdsToAdd.map(async (roleId) => findRoleById(roleId))); + await Promise.all( + roleIdsToRemove.map(async (roleId) => deleteUsersRolesByUserIdAndRoleId(userId, roleId)) + ); + await insertUsersRoles( + roleIdsToAdd.map((roleId) => ({ id: generateStandardId(), userId, roleId })) + ); + + ctx.status = 200; + + return next(); + } + ); + router.delete( '/users/:userId/roles/:roleId', koaGuard({