From cab82452ecb9aa6f89da22a1177cbf0e01f0462b Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 3 Apr 2025 19:22:49 +0900 Subject: [PATCH] Copy role on move (#15745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(backend): copyOnMoveAccount * feat(endpoints): copyOnMoveAccount * feat(frontend): copyOnMoveAccount * docs(changelog): アカウントのマイグレーション時に古いアカウントからロールをコピーできるようになりました。 * fix: spdx header for migration * Update locales/ja-JP.yml * copyOnMoveAccount -> preserveAssignmentOnMoveAccount * fix: check for preserveAssignmentOnMoveAccount --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 ++ locales/index.d.ts | 8 +++++ locales/ja-JP.yml | 2 ++ .../1743558299182-RoleCopyOnMoveAccount.js | 16 ++++++++++ .../backend/src/core/AccountMoveService.ts | 29 +++++++++++++++++++ packages/backend/src/core/RoleService.ts | 1 + .../src/core/entities/RoleEntityService.ts | 4 ++- packages/backend/src/models/Role.ts | 5 ++++ .../backend/src/models/json-schema/role.ts | 5 ++++ .../api/endpoints/admin/roles/create.ts | 1 + .../api/endpoints/admin/roles/update.ts | 2 ++ .../frontend/src/pages/admin/roles.editor.vue | 5 ++++ packages/misskey-js/src/autogen/types.ts | 4 +++ 13 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9da5e9bb2e..434a18442a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です - 参加中のルームをミュートして通知が来ないように設定可能です - メッセージにはリアクションも可能です +- Feat: アカウントのマイグレーション時に古いアカウントからロールをコピーできるようになりました。 + - 管理者がロールの設定でマイグレーション時にコピーするかを指定できるようになります。 - Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 - Misskeyネイティブでダッシュボードを実装予定です - Enhance: フロントエンドのエラートラッキングができるように diff --git a/locales/index.d.ts b/locales/index.d.ts index 9ef4886cb5..afaa2d975d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -7361,6 +7361,14 @@ export interface Locale extends ILocale { * 数値が大きいほどUI上で先頭に表示されます。 */ "descriptionOfDisplayOrder": string; + /** + * アサイン状態を移行先アカウントにも引き継ぐ + */ + "preserveAssignmentOnMoveAccount": string; + /** + * オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。 + */ + "preserveAssignmentOnMoveAccount_description": string; /** * モデレーターのメンバー編集を許可 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 947b7b4fa5..323dc3a38a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1907,6 +1907,8 @@ _role: descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。" displayOrder: "表示順" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" + preserveAssignmentOnMoveAccount: "アサイン状態を移行先アカウントにも引き継ぐ" + preserveAssignmentOnMoveAccount_description: "オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。" canEditMembersByModerator: "モデレーターのメンバー編集を許可" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" priority: "優先度" diff --git a/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js b/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js new file mode 100644 index 0000000000..ff4f7a051b --- /dev/null +++ b/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RoleCopyOnMoveAccount1743558299182 { + name = 'RoleCopyOnMoveAccount1743558299182' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "preserveAssignmentOnMoveAccount" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "preserveAssignmentOnMoveAccount"`); + } +} diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 0fbb9bcd80..406563bee8 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,32 @@ 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 + if (!role.preserveAssignmentOnMoveAccount) continue; + + 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..0a2659ee32 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, + preserveAssignmentOnMoveAccount: values.preserveAssignmentOnMoveAccount, canEditMembersByModerator: values.canEditMembersByModerator, displayOrder: values.displayOrder, policies: values.policies, diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 2a7dc37bce..3fa38c9521 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -13,6 +13,7 @@ import type { MiRole } from '@/models/Role.js'; import { bindThis } from '@/decorators.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; +import { Packed } from '@/misc/json-schema.js'; @Injectable() export class RoleEntityService { @@ -31,7 +32,7 @@ export class RoleEntityService { public async pack( src: MiRole['id'] | MiRole, me?: { id: MiUser['id'] } | null | undefined, - ) { + ): Promise> { const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') @@ -67,6 +68,7 @@ export class RoleEntityService { isModerator: role.isModerator, isExplorable: role.isExplorable, asBadge: role.asBadge, + preserveAssignmentOnMoveAccount: role.preserveAssignmentOnMoveAccount, canEditMembersByModerator: role.canEditMembersByModerator, displayOrder: role.displayOrder, policies: policies, diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts index a173971b2c..4c7da252bd 100644 --- a/packages/backend/src/models/Role.ts +++ b/packages/backend/src/models/Role.ts @@ -248,6 +248,11 @@ export class MiRole { }) public isExplorable: boolean; + @Column('boolean', { + default: false, + }) + public preserveAssignmentOnMoveAccount: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 1685a806c9..6f63dcef2e 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -389,6 +389,11 @@ export const packedRoleSchema = { optional: false, nullable: false, example: false, }, + preserveAssignmentOnMoveAccount: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, canEditMembersByModerator: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index e0c02f7a5d..f92f7ebaeb 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -36,6 +36,7 @@ export const paramDef = { isAdministrator: { type: 'boolean' }, isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility asBadge: { type: 'boolean' }, + preserveAssignmentOnMoveAccount: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, displayOrder: { type: 'number' }, policies: { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 465ad7aaaf..175adcb63f 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -41,6 +41,7 @@ export const paramDef = { isAdministrator: { type: 'boolean' }, isExplorable: { type: 'boolean' }, asBadge: { type: 'boolean' }, + preserveAssignmentOnMoveAccount: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, displayOrder: { type: 'number' }, policies: { @@ -78,6 +79,7 @@ export default class extends Endpoint { // eslint- isAdministrator: ps.isAdministrator, isExplorable: ps.isExplorable, asBadge: ps.asBadge, + preserveAssignmentOnMoveAccount: ps.preserveAssignmentOnMoveAccount, canEditMembersByModerator: ps.canEditMembersByModerator, displayOrder: ps.displayOrder, policies: ps.policies, diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index d1e823215a..73119940c1 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -52,6 +52,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 0d54c2d4ab..6bbf1e3319 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5135,6 +5135,8 @@ export type components = { /** @example false */ asBadge: boolean; /** @example false */ + preserveAssignmentOnMoveAccount: boolean; + /** @example false */ canEditMembersByModerator: boolean; policies: { [key: string]: { @@ -9392,6 +9394,7 @@ export type operations = { /** @default false */ isExplorable?: boolean; asBadge: boolean; + preserveAssignmentOnMoveAccount?: boolean; canEditMembersByModerator: boolean; displayOrder: number; policies: Record; @@ -9667,6 +9670,7 @@ export type operations = { isAdministrator?: boolean; isExplorable?: boolean; asBadge?: boolean; + preserveAssignmentOnMoveAccount?: boolean; canEditMembersByModerator?: boolean; displayOrder?: number; policies?: Record;