diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b147ed4b6..e9bb600ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ You should also include the user name that made the change. - Server: Add rate limit to i/notifications @tamaina - Client: Improve control panel @syuilo - Client: Show warning in control panel when there is an unresolved abuse report @syuilo +- Make possible to delete an account by admin @syuilo - Improve player detection in URL preview @mei23 - Add Badge Image to Push Notification #8012 @tamaina - Client: Removing entries from a clip @futchitwo diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7788a04dc3..f813389225 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -855,6 +855,8 @@ thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" recommended: "推奨" check: "チェック" isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" +typeToConfirm: "この操作を行うには {x} と入力してください" +deleteAccount: "アカウント削除" _emailUnavailable: used: "既に使用されています" diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 11d9d7c026..93f93cef0c 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -59,6 +59,7 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_vacuum from './endpoints/admin/vacuum.js'; +import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -370,6 +371,7 @@ const eps = [ ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], ['admin/vacuum', ep___admin_vacuum], + ['admin/delete-account', ep___admin_deleteAccount], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts new file mode 100644 index 0000000000..2d7ef2f236 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -0,0 +1,31 @@ +import { Users } from '@/models/index.js'; +import { deleteAccount } from '@/services/delete-account.js'; +import define from '../../define.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireAdmin: true, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps) => { + const user = await Users.findOneByOrFail({ id: ps.userId }); + if (user.isDeleted) { + return; + } + + await deleteAccount(user); +}); diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index 184005eb53..ede4a9d03b 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,9 +1,7 @@ import bcrypt from 'bcryptjs'; -import define from '../../define.js'; import { UserProfiles, Users } from '@/models/index.js'; -import { doPostSuspend } from '@/services/suspend-user.js'; -import { publishUserEvent } from '@/services/stream.js'; -import { createDeleteAccountJob } from '@/queue/index.js'; +import { deleteAccount } from '@/services/delete-account.js'; +import define from '../../define.js'; export const meta = { requireCredential: true, @@ -34,17 +32,5 @@ export default define(meta, paramDef, async (ps, user) => { throw new Error('incorrect password'); } - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch(e => {}); - - createDeleteAccountJob(user, { - soft: false, - }); - - await Users.update(user.id, { - isDeleted: true, - }); - - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); + await deleteAccount(user); }); diff --git a/packages/backend/src/services/delete-account.ts b/packages/backend/src/services/delete-account.ts new file mode 100644 index 0000000000..0fdceb671b --- /dev/null +++ b/packages/backend/src/services/delete-account.ts @@ -0,0 +1,23 @@ +import { Users } from '@/models/index.js'; +import { createDeleteAccountJob } from '@/queue/index.js'; +import { publishUserEvent } from './stream.js'; +import { doPostSuspend } from './suspend-user.js'; + +export async function deleteAccount(user: { + id: string; + host: string | null; +}): Promise { + // 物理削除する前にDelete activityを送信する + await doPostSuspend(user).catch(e => {}); + + createDeleteAccountJob(user, { + soft: false, + }); + + await Users.update(user.id, { + isDeleted: true, + }); + + // Terminate streaming + publishUserEvent(user.id, 'terminate', {}); +} diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 86c1be8d06..9dfb2d87a0 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -35,7 +35,10 @@ {{ $ts.silence }} {{ $ts.suspend }} {{ $ts.reflectMayTakeTime }} - {{ $ts.resetPassword }} +
+ {{ $ts.resetPassword }} + {{ $ts.deleteAccount }} +
@@ -233,6 +236,30 @@ async function deleteAllFiles() { await refreshUser(); } +async function deleteAccount() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteAccountConfirm, + }); + if (confirm.canceled) return; + + const typed = await os.inputText({ + text: i18n.t('typeToConfirm', { x: user?.username }), + }); + if (typed.canceled) return; + + if (typed.result === user?.username) { + await os.apiWithDialog('admin/delete-account', { + userId: user.id, + }); + } else { + os.alert({ + type: 'error', + text: 'input not match', + }); + } +} + watch(() => props.userId, () => { init = createFetcher(); }, {