diff --git a/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js b/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js new file mode 100644 index 0000000000..0ec1bf4dc3 --- /dev/null +++ b/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js @@ -0,0 +1,13 @@ +export class RoleCopyOnMoveAccount1743558299182 { + name = 'RoleCopyOnMoveAccount1743558299182' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "copyOnMoveAccount" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`COMMENT ON COLUMN "role"."copyOnMoveAccount" IS 'If true, the role will be copied to moved to the new user on moving a user.'`); + } + + async down(queryRunner) { + await queryRunner.query(`COMMENT ON COLUMN "role"."copyOnMoveAccount" IS 'If true, the role will be copied to moved to the new user on moving a user.'`); + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "copyOnMoveAccount"`); + } +} diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 0fbb9bcd80..f34a5a3032 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -24,6 +24,7 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { SystemAccountService } from '@/core/SystemAccountService.js'; +import { RoleService } from '@/core/RoleService.js'; @Injectable() export class AccountMoveService { @@ -61,6 +62,7 @@ export class AccountMoveService { private relayService: RelayService, private queueService: QueueService, private systemAccountService: SystemAccountService, + private roleService: RoleService, ) { } @@ -119,6 +121,7 @@ export class AccountMoveService { await Promise.all([ this.copyBlocking(src, dst), this.copyMutings(src, dst), + this.copyRoles(src, dst), this.updateLists(src, dst), ]); } catch { @@ -201,6 +204,31 @@ export class AccountMoveService { await this.mutingsRepository.insert(arrayToInsert); } + @bindThis + public async copyRoles(src: ThinUser, dst: ThinUser): Promise { + // Insert new roles with the same values except userId + // role service may have cache for roles so retrieve roles from service + const [oldRoleAssignments, roles] = await Promise.all([ + this.roleService.getUserAssigns(src.id), + this.roleService.getRoles(), + ]); + + if (oldRoleAssignments.length === 0) return; + + // No promise all since the only async operation is writing to the database + for (const oldRoleAssignment of oldRoleAssignments) { + const role = roles.find(x => x.id === oldRoleAssignment.roleId); + if (role == null) continue; // Very unlikely however removing role may cause this case + + try { + await this.roleService.assign(dst.id, role.id, oldRoleAssignment.expiresAt); + } catch (e) { + if (e instanceof RoleService.AlreadyAssignedError) continue; + throw e; + } + } + } + /** * Update lists while moving accounts. * - No removal of the old account from the lists diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 86f8a5caa1..ef30360fd7 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -630,6 +630,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { isModerator: values.isModerator, isExplorable: values.isExplorable, asBadge: values.asBadge, + copyOnMoveAccount: values.copyOnMoveAccount, canEditMembersByModerator: values.canEditMembersByModerator, displayOrder: values.displayOrder, policies: values.policies, diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts index a173971b2c..3eb34190dc 100644 --- a/packages/backend/src/models/Role.ts +++ b/packages/backend/src/models/Role.ts @@ -248,6 +248,12 @@ export class MiRole { }) public isExplorable: boolean; + @Column('boolean', { + default: false, + comment: 'If true, the role will be copied to moved to the new user on moving a user.', + }) + public copyOnMoveAccount: boolean; + @Column('boolean', { default: false, })