Copy role on move (#15745)

* 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>
This commit is contained in:
anatawa12 2025-04-03 19:22:49 +09:00 committed by GitHub
parent 440a4a4d8b
commit cab82452ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 83 additions and 1 deletions

View File

@ -11,6 +11,8 @@
- 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です - 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です
- 参加中のルームをミュートして通知が来ないように設定可能です - 参加中のルームをミュートして通知が来ないように設定可能です
- メッセージにはリアクションも可能です - メッセージにはリアクションも可能です
- Feat: アカウントのマイグレーション時に古いアカウントからロールをコピーできるようになりました。
- 管理者がロールの設定でマイグレーション時にコピーするかを指定できるようになります。
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 - Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です - Misskeyネイティブでダッシュボードを実装予定です
- Enhance: フロントエンドのエラートラッキングができるように - Enhance: フロントエンドのエラートラッキングができるように

8
locales/index.d.ts vendored
View File

@ -7361,6 +7361,14 @@ export interface Locale extends ILocale {
* UI上で先頭に表示されます * UI上で先頭に表示されます
*/ */
"descriptionOfDisplayOrder": string; "descriptionOfDisplayOrder": string;
/**
*
*/
"preserveAssignmentOnMoveAccount": string;
/**
*
*/
"preserveAssignmentOnMoveAccount_description": string;
/** /**
* *
*/ */

View File

@ -1907,6 +1907,8 @@ _role:
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。" descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
displayOrder: "表示順" displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
preserveAssignmentOnMoveAccount: "アサイン状態を移行先アカウントにも引き継ぐ"
preserveAssignmentOnMoveAccount_description: "オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度" priority: "優先度"

View File

@ -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"`);
}
}

View File

@ -24,6 +24,7 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable() @Injectable()
export class AccountMoveService { export class AccountMoveService {
@ -61,6 +62,7 @@ export class AccountMoveService {
private relayService: RelayService, private relayService: RelayService,
private queueService: QueueService, private queueService: QueueService,
private systemAccountService: SystemAccountService, private systemAccountService: SystemAccountService,
private roleService: RoleService,
) { ) {
} }
@ -119,6 +121,7 @@ export class AccountMoveService {
await Promise.all([ await Promise.all([
this.copyBlocking(src, dst), this.copyBlocking(src, dst),
this.copyMutings(src, dst), this.copyMutings(src, dst),
this.copyRoles(src, dst),
this.updateLists(src, dst), this.updateLists(src, dst),
]); ]);
} catch { } catch {
@ -201,6 +204,32 @@ export class AccountMoveService {
await this.mutingsRepository.insert(arrayToInsert); await this.mutingsRepository.insert(arrayToInsert);
} }
@bindThis
public async copyRoles(src: ThinUser, dst: ThinUser): Promise<void> {
// 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. * Update lists while moving accounts.
* - No removal of the old account from the lists * - No removal of the old account from the lists

View File

@ -630,6 +630,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
isModerator: values.isModerator, isModerator: values.isModerator,
isExplorable: values.isExplorable, isExplorable: values.isExplorable,
asBadge: values.asBadge, asBadge: values.asBadge,
preserveAssignmentOnMoveAccount: values.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: values.canEditMembersByModerator, canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder, displayOrder: values.displayOrder,
policies: values.policies, policies: values.policies,

View File

@ -13,6 +13,7 @@ import type { MiRole } from '@/models/Role.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { Packed } from '@/misc/json-schema.js';
@Injectable() @Injectable()
export class RoleEntityService { export class RoleEntityService {
@ -31,7 +32,7 @@ export class RoleEntityService {
public async pack( public async pack(
src: MiRole['id'] | MiRole, src: MiRole['id'] | MiRole,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
) { ): Promise<Packed<'Role'>> {
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
@ -67,6 +68,7 @@ export class RoleEntityService {
isModerator: role.isModerator, isModerator: role.isModerator,
isExplorable: role.isExplorable, isExplorable: role.isExplorable,
asBadge: role.asBadge, asBadge: role.asBadge,
preserveAssignmentOnMoveAccount: role.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
displayOrder: role.displayOrder, displayOrder: role.displayOrder,
policies: policies, policies: policies,

View File

@ -248,6 +248,11 @@ export class MiRole {
}) })
public isExplorable: boolean; public isExplorable: boolean;
@Column('boolean', {
default: false,
})
public preserveAssignmentOnMoveAccount: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View File

@ -389,6 +389,11 @@ export const packedRoleSchema = {
optional: false, nullable: false, optional: false, nullable: false,
example: false, example: false,
}, },
preserveAssignmentOnMoveAccount: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
canEditMembersByModerator: { canEditMembersByModerator: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -36,6 +36,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
asBadge: { type: 'boolean' }, asBadge: { type: 'boolean' },
preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' }, displayOrder: { type: 'number' },
policies: { policies: {

View File

@ -41,6 +41,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean' }, isExplorable: { type: 'boolean' },
asBadge: { type: 'boolean' }, asBadge: { type: 'boolean' },
preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' }, displayOrder: { type: 'number' },
policies: { policies: {
@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
isExplorable: ps.isExplorable, isExplorable: ps.isExplorable,
asBadge: ps.asBadge, asBadge: ps.asBadge,
preserveAssignmentOnMoveAccount: ps.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder, displayOrder: ps.displayOrder,
policies: ps.policies, policies: ps.policies,

View File

@ -52,6 +52,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </MkFolder>
<MkSwitch v-model="role.preserveAssignmentOnMoveAccount" :readonly="readonly">
<template #label>{{ i18n.ts._role.preserveAssignmentOnMoveAccount }}</template>
<template #caption>{{ i18n.ts._role.preserveAssignmentOnMoveAccount_description }}</template>
</MkSwitch>
<MkSwitch v-model="role.canEditMembersByModerator" :readonly="readonly"> <MkSwitch v-model="role.canEditMembersByModerator" :readonly="readonly">
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template> <template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template> <template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>

View File

@ -5135,6 +5135,8 @@ export type components = {
/** @example false */ /** @example false */
asBadge: boolean; asBadge: boolean;
/** @example false */ /** @example false */
preserveAssignmentOnMoveAccount: boolean;
/** @example false */
canEditMembersByModerator: boolean; canEditMembersByModerator: boolean;
policies: { policies: {
[key: string]: { [key: string]: {
@ -9392,6 +9394,7 @@ export type operations = {
/** @default false */ /** @default false */
isExplorable?: boolean; isExplorable?: boolean;
asBadge: boolean; asBadge: boolean;
preserveAssignmentOnMoveAccount?: boolean;
canEditMembersByModerator: boolean; canEditMembersByModerator: boolean;
displayOrder: number; displayOrder: number;
policies: Record<string, never>; policies: Record<string, never>;
@ -9667,6 +9670,7 @@ export type operations = {
isAdministrator?: boolean; isAdministrator?: boolean;
isExplorable?: boolean; isExplorable?: boolean;
asBadge?: boolean; asBadge?: boolean;
preserveAssignmentOnMoveAccount?: boolean;
canEditMembersByModerator?: boolean; canEditMembersByModerator?: boolean;
displayOrder?: number; displayOrder?: number;
policies?: Record<string, never>; policies?: Record<string, never>;