enhance(role): ロールの割り当て時メモを残せるように (MisskeyIO#842)

This commit is contained in:
あわわわとーにゅ 2024-12-25 09:42:59 +09:00 committed by GitHub
parent 346c848134
commit 6542ad4a12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 72 additions and 7 deletions

View file

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

View file

@ -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<void> {
public async assign(userId: MiUser['id'], roleId: MiRole['id'], memo: string | null = null, expiresAt: Date | null = null, moderator?: MiUser): Promise<void> {
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,
});
}
}

View file

@ -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;
}

View file

@ -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<typeof meta, typeof paramDef> { // 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);
});
}
}

View file

@ -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<typeof meta, typeof paramDef> { // 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,
})));
});

View file

@ -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<typeof meta, typeof paramDef> { // eslint-
createdAt: this.idService.parse(a.id).date.toISOString(),
expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null,
roleId: a.roleId,
memo: a.memo,
})),
};
});

View file

@ -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;

View file

@ -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,

View file

@ -133,6 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub">
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).memo">Memo: {{ info.roleAssigns.find(a => a.roleId === role.id).memo }}</div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div>
@ -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);
}

View file

@ -90,10 +90,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else-if="log.type === 'assignRole'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div>{{ i18n.ts.role }}: {{ log.info.roleName }} [{{ log.info.roleId }}]</div>
<div>{{ i18n.ts.memo }}: {{ log.info.memo }}</div>
<div>{{ i18n.ts.expirationDate }}: {{ log.info.expiresAt ?? i18n.ts.indefinitely }}</div>
</template>
<template v-else-if="log.type === 'unassignRole'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div>{{ i18n.ts.role }}: {{ log.info.roleName }} [{{ log.info.roleId }}]</div>
<div>{{ i18n.ts.memo }}: {{ log.info.memo }}</div>
</template>
<template v-else-if="log.type === 'updateCustomEmoji'">
<div>{{ i18n.ts.emoji }}: {{ log.info.emojiId }}</div>

View file

@ -45,6 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub">
<div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div>
<div v-if="item.memo">Memo: {{ item.memo }}</div>
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div>
@ -142,7 +143,14 @@ async function assign() {
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
: null;
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt });
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: role.id, userId: user.id, memo: memo ?? undefined, expiresAt });
//role.users.push(user);
}

View file

@ -295,7 +295,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
: null;
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt });
const { canceled: canceled3, result: memo } = await os.inputText({
title: i18n.ts.addMemo,
type: 'textarea',
placeholder: i18n.ts.memo,
});
if (canceled3) return;
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, memo: memo ?? undefined, expiresAt });
},
}));
},

View file

@ -9830,6 +9830,7 @@ export type operations = {
createdAt: string;
expiresAt: string | null;
roleId: string;
memo: string | null;
})[];
};
};
@ -10619,6 +10620,7 @@ export type operations = {
roleId: string;
/** Format: misskey:id */
userId: string;
memo?: string;
expiresAt?: number | null;
};
};
@ -10796,6 +10798,7 @@ export type operations = {
/** Format: date-time */
createdAt: string;
user: components['schemas']['UserDetailed'];
memo: string | null;
/** Format: date-time */
expiresAt: string | null;
})[];

View file

@ -202,6 +202,7 @@ export type ModerationLogPayloads = {
roleId: string;
roleName: string;
expiresAt: string | null;
memo: string | null;
};
unassignRole: {
userId: string;
@ -209,6 +210,7 @@ export type ModerationLogPayloads = {
userHost: string | null;
roleId: string;
roleName: string;
memo: string | null;
};
createRole: {
roleId: string;