diff --git a/locales/en-US.yml b/locales/en-US.yml index 09fd726c9..2aba028e4 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -564,6 +564,10 @@ output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" updateRemoteUser: "Update remote user information" +deleteUserAvatar: "Delete user icon" +deleteUserAvatarConfirm: "Are you sure that you want to delete this user's icon?" +deleteUserBanner: "Delete user banner" +deleteUserBannerConfirm: "Are you sure that you want to delete this user's banner?" deleteAllFiles: "Delete all files" deleteAllFilesConfirm: "Are you sure that you want to delete all files?" removeAllFollowing: "Unfollow all followed users" diff --git a/locales/index.d.ts b/locales/index.d.ts index 6baed91c4..6fd6d3641 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -567,6 +567,10 @@ export interface Locale { "script": string; "disablePagesScript": string; "updateRemoteUser": string; + "deleteUserAvatar": string; + "deleteUserAvatarConfirm": string; + "deleteUserBanner": string; + "deleteUserBannerConfirm": string; "deleteAllFiles": string; "deleteAllFilesConfirm": string; "removeAllFollowing": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e59a550df..9685e9c5a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -564,6 +564,10 @@ output: "出力" script: "スクリプト" disablePagesScript: "Pagesのスクリプトを無効にする" updateRemoteUser: "リモートユーザー情報の更新" +deleteUserAvatar: "アイコンを削除" +deleteUserAvatarConfirm: "アイコンを削除しますか?" +deleteUserBanner: "バナーを削除" +deleteUserBannerConfirm: "バナーを削除しますか?" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 6d813ae04..3797b46d0 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js'; +import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -383,6 +385,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; +const $admin_deleteUserAvatar: Provider = { provide: 'ep:admin/delete-user-avatar', useClass: ep___admin_deleteUserAvatar.default }; +const $admin_deleteUserBanner: Provider = { provide: 'ep:admin/delete-user-banner', useClass: ep___admin_deleteUserBanner.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -746,6 +750,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_deleteUserAvatar, + $admin_deleteUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1103,6 +1109,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_deleteUserAvatar, + $admin_deleteUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index ef4bd3e2b..4162ace33 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js'; +import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -381,6 +383,8 @@ const eps = [ ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], + ['admin/delete-user-avatar', ep___admin_deleteUserAvatar], + ['admin/delete-user-banner', ep___admin_deleteUserBanner], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/files', ep___admin_drive_files], diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts new file mode 100644 index 000000000..d3c78d7fb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} 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 +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await this.usersRepository.update(user.id, { + avatar: null, + avatarId: null, + avatarUrl: null, + avatarBlurhash: null, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts new file mode 100644 index 000000000..e076cdcfc --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} 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 +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await this.usersRepository.update(user.id, { + banner: null, + bannerId: null, + bannerUrl: null, + bannerBlurhash: null, + }); + }); + } +} diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 5d671acf3..9f4975e88 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -122,6 +122,10 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ {{ i18n.ts.deleteUserAvatar }} + {{ i18n.ts.deleteUserBanner }} +
{{ i18n.ts.deleteAccount }} @@ -320,6 +324,44 @@ async function toggleSuspend(v) { } } +async function deleteUserAvatar() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteUserAvatarConfirm, + }); + if (confirm.canceled) return; + const process = async () => { + await os.api('admin/delete-user-avatar', { userId: user.id }); + os.success(); + }; + await process().catch(err => { + os.alert({ + type: 'error', + text: err.toString(), + }); + }); + refreshUser(); +} + +async function deleteUserBanner() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteUserBannerConfirm, + }); + if (confirm.canceled) return; + const process = async () => { + await os.api('admin/delete-user-banner', { userId: user.id }); + os.success(); + }; + await process().catch(err => { + os.alert({ + type: 'error', + text: err.toString(), + }); + }); + refreshUser(); +} + async function deleteAllFiles() { const confirm = await os.confirm({ type: 'warning', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 87922ba79..f4bcaa806 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -345,6 +345,18 @@ export type Endpoints = { }; res: null; }; + 'admin/delete-user-avatar': { + req: { + userId: User['id']; + }; + res: null; + }; + 'admin/delete-user-banner': { + req: { + userId: User['id']; + }; + res: null; + }; 'admin/delete-logs': { req: NoParams; res: null; diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index 54b175fcf..e3f28c644 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -15,6 +15,8 @@ export type Endpoints = { // admin 'admin/abuse-user-reports': { req: TODO; res: TODO; }; 'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; }; + 'admin/delete-user-avatar': { req: { userId: User['id']; }; res: null; }; + 'admin/delete-user-banner': { req: { userId: User['id']; }; res: null; }; 'admin/delete-logs': { req: NoParams; res: null; }; 'admin/get-index-stats': { req: TODO; res: TODO; }; 'admin/get-table-stats': { req: TODO; res: TODO; };