diff --git a/packages/backend/migration/1735078824104-RoleAssignment-Memo.js b/packages/backend/migration/1735078824104-RoleAssignment-Memo.js new file mode 100644 index 000000000..87599f09b --- /dev/null +++ b/packages/backend/migration/1735078824104-RoleAssignment-Memo.js @@ -0,0 +1,13 @@ +export class RoleAssignmentMemo1735078824104 { + name = 'RoleAssignmentMemo1735078824104' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role_assignment" ADD "memo" character varying(256)`); + await queryRunner.query(`COMMENT ON COLUMN "role_assignment"."memo" IS 'memo for the role assignment'`); + } + + async down(queryRunner) { + await queryRunner.query(`COMMENT ON COLUMN "role_assignment"."memo" IS 'memo for the role assignment'`); + await queryRunner.query(`ALTER TABLE "role_assignment" DROP COLUMN "memo"`); + } +} diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 9089480f9..1c205dca0 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -488,7 +488,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise { + public async assign(userId: MiUser['id'], roleId: MiRole['id'], memo: string | null = null, expiresAt: Date | null = null, moderator?: MiUser): Promise { const now = Date.now(); const role = await this.rolesRepository.findOneByOrFail({ id: roleId }); @@ -512,6 +512,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { expiresAt: expiresAt, roleId: roleId, userId: userId, + memo: memo, }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('userRoleAssigned', created); @@ -521,9 +522,10 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { roleId: roleId, }); } - } else if (existing.expiresAt !== expiresAt) { + } else if (existing.expiresAt !== expiresAt || existing.memo !== memo) { await this.roleAssignmentsRepository.update(existing.id, { expiresAt: expiresAt, + memo: memo, }); } else { throw new IdentifiableError('67d8689c-25c6-435f-8ced-631e4b81fce1', 'User is already assigned to this role.'); @@ -542,6 +544,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { userUsername: user.username, userHost: user.host, expiresAt: expiresAt ? expiresAt.toISOString() : null, + memo: memo, }); } } @@ -582,6 +585,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { userId: userId, userUsername: user.username, userHost: user.host, + memo: existing.memo, }); } } diff --git a/packages/backend/src/models/RoleAssignment.ts b/packages/backend/src/models/RoleAssignment.ts index b74fd90b9..67792725d 100644 --- a/packages/backend/src/models/RoleAssignment.ts +++ b/packages/backend/src/models/RoleAssignment.ts @@ -52,4 +52,11 @@ export class MiRoleAssignment { nullable: true, }) public expiresAt: Date | null; + + @Column('varchar', { + comment: 'memo for the role assignment', + length: 256, + nullable: true, + }) + public memo: string | null; } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts index df5955efd..b1f8a2ec6 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -49,6 +49,7 @@ export const paramDef = { properties: { roleId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' }, + memo: { type: 'string' }, expiresAt: { type: 'integer', nullable: true, @@ -90,7 +91,7 @@ export default class extends Endpoint { // eslint- return; } - await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null, me); + await this.roleService.assign(user.id, role.id, ps.memo, ps.expiresAt ? new Date(ps.expiresAt) : null, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 45758d4f5..f126e5652 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -36,6 +36,7 @@ export const meta = { id: { type: 'string', format: 'misskey:id' }, createdAt: { type: 'string', format: 'date-time' }, user: { ref: 'UserDetailed' }, + memo: { type: 'string', nullable: true }, expiresAt: { type: 'string', format: 'date-time', nullable: true }, }, required: ['id', 'createdAt', 'user'], @@ -93,6 +94,7 @@ export default class extends Endpoint { // eslint- id: assign.id, createdAt: this.idService.parse(assign.id).date.toISOString(), user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), + memo: assign.memo, expiresAt: assign.expiresAt?.toISOString() ?? null, }))); }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index f1818086a..f2c40541f 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -177,6 +177,10 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + memo: { + type: 'string', + optional: false, nullable: true, + } }, }, }, @@ -262,6 +266,7 @@ export default class extends Endpoint { // eslint- createdAt: this.idService.parse(a.id).date.toISOString(), expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null, roleId: a.roleId, + memo: a.memo, })), }; }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 9e595e83e..e125b074f 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -150,6 +150,7 @@ export type ModerationLogPayloads = { roleId: string; roleName: string; expiresAt: string | null; + memo: string | null; }; unassignRole: { userId: string; @@ -157,6 +158,7 @@ export type ModerationLogPayloads = { userHost: string | null; roleId: string; roleName: string; + memo: string | null; }; createRole: { roleId: string; diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index e0b7621b2..d27211302 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -239,7 +239,7 @@ describe('RoleService', () => { }, }, }); - await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24))); + await roleService.assign(user.id, role.id, 'test', new Date(Date.now() + (1000 * 60 * 60 * 24))); metaService.fetch.mockResolvedValue({ policies: { canManageCustomEmojis: false, diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index b4e58d99f..09a51efdc 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -133,6 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Assigned:
+
Memo: {{ info.roleAssigns.find(a => a.roleId === role.id).memo }}
Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}
Period: {{ i18n.ts.indefinitely }}
@@ -502,8 +503,15 @@ async function assignRole() { : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) : null; + const { canceled: canceled3, result: memo } = await os.inputText({ + title: i18n.ts.addMemo, + type: 'textarea', + placeholder: i18n.ts.memo, + }); + if (canceled3) return; + await os.apiWithDialog('admin/roles/assign', { - roleId, userId: user.value.id, expiresAt, + roleId, userId: user.value.id, memo: memo ?? undefined, expiresAt, }).then(refreshUser); } diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 7cfb31506..849bbc6e7 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -90,10 +90,13 @@ SPDX-License-Identifier: AGPL-3.0-only