diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index b250eeee21..f15c5ab45f 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -737,6 +737,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } } + @bindThis + public async clone(role: MiRole, moderator?: MiUser): Promise { + let newName = `${role.name} (cloned)`; + + if (newName.length > 256) { + newName = newName.slice(0, 256); + } + + return this.create({ + ...role, + name: newName, + }, moderator); + } + @bindThis public async delete(role: MiRole, moderator?: MiUser): Promise { await this.rolesRepository.delete({ id: role.id }); diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index a78c3e9ae6..962055052d 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -88,6 +88,7 @@ export * as 'admin/reset-password' from './endpoints/admin/reset-password.js'; export * as 'admin/resolve-abuse-user-report' from './endpoints/admin/resolve-abuse-user-report.js'; export * as 'admin/roles/assign' from './endpoints/admin/roles/assign.js'; export * as 'admin/roles/create' from './endpoints/admin/roles/create.js'; +export * as 'admin/roles/clone' from './endpoints/admin/roles/clone.js'; export * as 'admin/roles/delete' from './endpoints/admin/roles/delete.js'; export * as 'admin/roles/list' from './endpoints/admin/roles/list.js'; export * as 'admin/roles/show' from './endpoints/admin/roles/show.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/roles/clone.ts b/packages/backend/src/server/api/endpoints/admin/roles/clone.ts new file mode 100644 index 0000000000..83a7a54c3a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/clone.ts @@ -0,0 +1,60 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { RoleService } from '@/core/RoleService.js'; +import { DI } from '@/di-symbols.js'; +import type { RolesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['admin', 'role'], + + requireCredential: true, + requireAdmin: true, + kind: 'write:admin:roles', + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '93cc897a-b5f9-431f-b9b7-ee59035a5aed', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + }, + required: [ + 'roleId', + ], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + private roleEntityService: RoleEntityService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + const cloned = await this.roleService.clone(role, me); + + return this.roleEntityService.pack(cloned, me); + }); + } +} diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 2afe22618d..534556b056 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -983,4 +983,50 @@ describe('RoleService', () => { expect(notificationService.createNotification).not.toHaveBeenCalled(); }); }); + + describe('clone', () => { + test('clones an role', async () => { + const role = await createRole({ + name: 'original role', + color: '#ff0000', + policies: { + canManageCustomEmojis: { + useDefault: false, + priority: 0, + value: true, + }, + }, + }); + + const clonedRole = await roleService.clone(role); + + expect(clonedRole).toBeDefined(); + expect(clonedRole.id).not.toBe(role.id); + expect(clonedRole.name).toBe(`${role.name} (cloned)`); + + expect(clonedRole).toEqual(expect.objectContaining({ + color: role.color, + policies: { + canManageCustomEmojis: { + useDefault: false, + priority: 0, + value: true, + }, + }, + })); + }); + + test('clones a roll with a too long name', async () => { + const role = await createRole({ + name: 'a'.repeat(254), + }); + + const clonedRole = await roleService.clone(role); + + expect(clonedRole).toBeDefined(); + expect(clonedRole.id).not.toBe(role.id); + expect(clonedRole.name).toBe(`${role.name} (`); + expect(clonedRole.name.length).toBe(256); + }); + }); });