From 13e4702c073ab2ba3fe80bef19c7254e01724536 Mon Sep 17 00:00:00 2001 From: ZerglingGo Date: Sat, 2 Dec 2023 18:15:40 +0900 Subject: [PATCH] feat: truncate account from bscone fork --- locales/index.d.ts | 30 ++++ locales/ja-JP.yml | 9 ++ locales/ko-KR.yml | 10 +- packages/backend/src/core/CoreModule.ts | 6 + packages/backend/src/core/QueueService.ts | 10 ++ .../src/core/TruncateAccountService.ts | 33 +++++ .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 3 + .../TruncateAccountProcessorService.ts | 136 ++++++++++++++++++ packages/backend/src/queue/types.ts | 5 + .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/i/truncate-account.ts | 70 +++++++++ .../frontend/src/pages/settings/other.vue | 62 ++++++-- .../frontend/src/pages/settings/privacy.vue | 15 -- packages/misskey-js/etc/misskey-js.api.md | 4 + .../misskey-js/src/autogen/apiClientJSDoc.ts | 12 ++ packages/misskey-js/src/autogen/endpoint.ts | 2 + packages/misskey-js/src/autogen/entities.ts | 1 + packages/misskey-js/src/autogen/types.ts | 63 ++++++++ 20 files changed, 454 insertions(+), 25 deletions(-) create mode 100644 packages/backend/src/core/TruncateAccountService.ts create mode 100644 packages/backend/src/queue/processors/TruncateAccountProcessorService.ts create mode 100644 packages/backend/src/server/api/endpoints/i/truncate-account.ts diff --git a/locales/index.d.ts b/locales/index.d.ts index c4368fdbd..fa78c6bcb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -3664,6 +3664,10 @@ export interface Locale extends ILocale { * アカウントが削除されます。よろしいですか? */ "deleteAccountConfirm": string; + /** + * ダイレクトとピン留めされたノート、関連ドライブのファイルを除くすべてのノートとドライブのファイルが削除されます。続行しますか? + */ + "truncateAccountConfirm": string; /** * パスワードが間違っています。 */ @@ -3848,6 +3852,10 @@ export interface Locale extends ILocale { * アカウント削除 */ "deleteAccount": string; + /** + * アカウント整理 + */ + "truncateAccount": string; /** * ドキュメント */ @@ -7292,6 +7300,28 @@ export interface Locale extends ILocale { */ "youCantUseThisTime": string; }; + "_accountTruncate": { + /** + * アカウントの整理 + */ + "accountDelete": string; + /** + * アカウントの整理は負荷のかかる処理であるため、作成したコンテンツの数やアップロードしたファイルの数が多いと完了までに時間がかかることがあります。 + */ + "mayTakeTime": string; + /** + * アカウント整理をリクエスト + */ + "requestAccountTruncate": string; + /** + * 整理処理が開始されました。 + */ + "started": string; + /** + * 整理が進行中 + */ + "inProgress": string; + }; "_ad": { /** * 戻る diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a1b55acc4..f432753c6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -912,6 +912,7 @@ followingVisibility: "フォローの公開範囲" followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" +truncateAccountConfirm: "ダイレクトとピン留めされたノート、関連ドライブのファイルを除くすべてのノートとドライブのファイルが削除されます。続行しますか?" incorrectPassword: "パスワードが間違っています。" voteConfirm: "「{choice}」に投票しますか?" hide: "隠す" @@ -958,6 +959,7 @@ requireAdminForView: "閲覧するには管理者アカウントでログイン isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" typeToConfirm: "この操作を行うには {x} と入力してください" deleteAccount: "アカウント削除" +truncateAccount: "アカウント整理" document: "ドキュメント" numberOfPageCache: "ページキャッシュ数" numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" @@ -1889,6 +1891,13 @@ _accountDelete: inProgress: "削除が進行中" youCantUseThisTime: "現在、アカウントの削除はできません。" +_accountTruncate: + accountDelete: "アカウントの整理" + mayTakeTime: "アカウントの整理は負荷のかかる処理であるため、作成したコンテンツの数やアップロードしたファイルの数が多いと完了までに時間がかかることがあります。" + requestAccountTruncate: "アカウント整理をリクエスト" + started: "整理処理が開始されました。" + inProgress: "整理が進行中" + _ad: back: "戻る" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 52833a676..e8d0ffcbe 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -909,6 +909,7 @@ followingVisibility: "팔로우 중인 유저를 볼 수 있는 사람" followersVisibility: "내 팔로워를 볼 수 있는 사람" continueThread: "글타래 더 보기" deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? " +truncateAccountConfirm: "다이렉트 및 프로필 상단에 고정된 노트를 제외한 모든 노트와 파일을 제거하고 복구할 수 없습니다. 그래도 계속하시겠습니까?" incorrectPassword: "비밀번호가 올바르지 않습니다." voteConfirm: "\"{choice}\"에 투표하시겠습니까?" hide: "숨기기" @@ -948,13 +949,14 @@ noEmailServerWarning: "메일 서버가 설정되어 있지 않습니다." thereIsUnresolvedAbuseReportWarning: "해결되지 않은 신고가 있습니다." thereIsUnresolvedAbuseReport: "아직 해결되지 않은 신고가 {left}건 있습니다. 제어판에서 확인해주세요." recommended: "추천" -check: "체크" +check: "확인" driveCapOverrideLabel: "이 유저의 드라이브 용량을 변경" driveCapOverrideCaption: "0 이하를 지정하면 해제됩니다." requireAdminForView: "열람하려면 관리자 계정으로 로그인해야 합니다." isSystemAccount: "시스템에 의해 자동으로 생성되어 관리되는 계정입니다." typeToConfirm: "계속하시려면 {x} 을 입력하세요" deleteAccount: "계정 삭제" +truncateAccount: "계정 청소" document: "문서" numberOfPageCache: "페이지 캐시 수" numberOfPageCacheDescription: "숫자가 클 수록 편리성이 높아지지만, 시스템 자원과 메모리를 더 많이 사용합니다." @@ -1857,6 +1859,12 @@ _accountDelete: started: "삭제 작업이 시작되었습니다." inProgress: "삭제 진행 중" youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다." +_accountTruncate: + accountTruncate: "계정 청소" + mayTakeTime: "계정 청소는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다." + requestAccountTruncate: "계정 청소 요청" + started: "청소 작업이 시작되었습니다." + inProgress: "청소 진행 중" _ad: back: "뒤로" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 3e5223944..92b068b14 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -17,6 +17,7 @@ import { CaptchaService } from './CaptchaService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CustomEmojiService } from './CustomEmojiService.js'; import { DeleteAccountService } from './DeleteAccountService.js'; +import { TruncateAccountService } from './TruncateAccountService.js'; import { DownloadService } from './DownloadService.js'; import { DriveService } from './DriveService.js'; import { EmailService } from './EmailService.js'; @@ -155,6 +156,7 @@ const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: Capt const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; +const $TruncateAccountService: Provider = { provide: 'TruncateAccountService', useExisting: TruncateAccountService }; const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService }; const $DriveService: Provider = { provide: 'DriveService', useExisting: DriveService }; const $EmailService: Provider = { provide: 'EmailService', useExisting: EmailService }; @@ -297,6 +299,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting CreateSystemUserService, CustomEmojiService, DeleteAccountService, + TruncateAccountService, DownloadService, DriveService, EmailService, @@ -433,6 +436,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, + $TruncateAccountService, $DownloadService, $DriveService, $EmailService, @@ -570,6 +574,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting CreateSystemUserService, CustomEmojiService, DeleteAccountService, + TruncateAccountService, DownloadService, DriveService, EmailService, @@ -705,6 +710,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, + $TruncateAccountService, $DownloadService, $DriveService, $EmailService, diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 46952a4ac..bbed9641a 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -389,6 +389,16 @@ export class QueueService { ); } + @bindThis + public createTruncateAccountJob(user: ThinUser, opts = {}) { + return this.dbQueue.add('truncateAccount', { + user: { id: user.id }, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + @bindThis public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) { const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel)); diff --git a/packages/backend/src/core/TruncateAccountService.ts b/packages/backend/src/core/TruncateAccountService.ts new file mode 100644 index 000000000..ef042cf25 --- /dev/null +++ b/packages/backend/src/core/TruncateAccountService.ts @@ -0,0 +1,33 @@ +/* + * 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 { QueueService } from '@/core/QueueService.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class TruncateAccountService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private queueService: QueueService, + ) { + } + + @bindThis + public async truncateAccount(user: { + id: string; + host: string | null; + }): Promise { + const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); + + this.queueService.createTruncateAccountJob(user, { + soft: false, + }); + } +} diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 1cbab81e5..0f036600a 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -17,6 +17,7 @@ import { CleanChartsProcessorService } from './processors/CleanChartsProcessorSe import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; +import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; import { ReindexNotesProcessorService } from './processors/ReindexNotesProcessorService.js'; @@ -72,6 +73,7 @@ import { AutoNoteRemovalProcessorService } from './processors/AutoNoteRemovalPro ImportCustomEmojisProcessorService, ImportAntennasProcessorService, DeleteAccountProcessorService, + TruncateAccountProcessorService, DeleteFileProcessorService, CleanRemoteFilesProcessorService, RelationshipProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 05a292eaa..1c51d6160 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -30,6 +30,7 @@ import { ImportUserListsProcessorService } from './processors/ImportUserListsPro import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; +import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; @@ -110,6 +111,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService, private importAntennasProcessorService: ImportAntennasProcessorService, private deleteAccountProcessorService: DeleteAccountProcessorService, + private truncateAccountProcessorService: TruncateAccountProcessorService, private deleteFileProcessorService: DeleteFileProcessorService, private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService, private relationshipProcessorService: RelationshipProcessorService, @@ -191,6 +193,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'importAntennas': return this.importAntennasProcessorService.process(job); case 'deleteAccount': return this.deleteAccountProcessorService.process(job); case 'reportAbuse': return this.reportAbuseProcessorService.process(job); + case 'truncateAccount': return this.truncateAccountProcessorService.process(job); default: throw new Error(`unrecognized job type ${job.name} for db`); } }, { diff --git a/packages/backend/src/queue/processors/TruncateAccountProcessorService.ts b/packages/backend/src/queue/processors/TruncateAccountProcessorService.ts new file mode 100644 index 000000000..4b2f33b98 --- /dev/null +++ b/packages/backend/src/queue/processors/TruncateAccountProcessorService.ts @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { And, In, MoreThan, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { DriveFilesRepository, NotesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNote } from '@/models/Note.js'; +import { bindThis } from '@/decorators.js'; +import { NoteDeleteService } from '@/core/NoteDeleteService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbUserTruncateJobData } from '../types.js'; + +@Injectable() +export class TruncateAccountProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.userNotePiningsRepository) + private userNotePiningsRepository: UserNotePiningsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private driveService: DriveService, + private queueLoggerService: QueueLoggerService, + private noteDeleteService: NoteDeleteService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('truncate-account'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + this.logger.info(`Truncate notes and drives account of ${job.data.user.id} ...`); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + return; + } + + const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id }); + const piningNoteIds = pinings.map(pining => pining.noteId); // pining.note always undefined (bug?) + + const specifiedNotes = await this.notesRepository.findBy({ + userId: user.id, + visibility: Not(In(['public', 'home', 'followers'])), + }); + const specifiedNoteIds = specifiedNotes.map(note => note.id); + + const keepFileIds = (await Promise.all([...piningNoteIds, ...specifiedNoteIds].map(async (noteId) => { + const note = await this.notesRepository.findOneBy({ id: noteId }); + + return note?.fileIds; + }))).flat().filter((fileId) => fileId !== undefined); + + { // Delete notes + let cursor: MiNote['id'] | null = null; + + while (true) { + const notes = await this.notesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { + id: And(Not(In([...piningNoteIds, ...specifiedNoteIds])), MoreThan(cursor)), + } : { + id: Not(In([...piningNoteIds, ...specifiedNoteIds])), + }), + }, + take: 100, + order: { + id: 1, + }, + }) as MiNote[]; + + if (notes.length === 0) { + break; + } + + cursor = notes.at(-1)?.id ?? null; + + await Promise.all(notes.map((note) => { + return this.noteDeleteService.delete(user, note, false, user); + })); + } + + this.logger.succ('All of notes deleted'); + } + + { // Delete files + let cursor: MiDriveFile['id'] | null = null; + + while (true) { + const files = await this.driveFilesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { + id: And(Not(In(keepFileIds)), MoreThan(cursor)), + } : { + id: Not(In(keepFileIds)), + }), + }, + take: 10, + order: { + id: 1, + }, + }) as MiDriveFile[]; + + if (files.length === 0) { + break; + } + + cursor = files.at(-1)?.id ?? null; + + for (const file of files) { + await this.driveService.deleteFileSync(file); + } + } + + this.logger.succ('All of files deleted'); + } + + return 'Account notes and drives are truncated'; + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 954168964..0829d6180 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -60,6 +60,7 @@ export type DbJobMap = { importUserLists: DbUserImportJobData; importCustomEmojis: DbUserImportJobData; deleteAccount: DbUserDeleteJobData; + truncateAccount: DbUserTruncateJobData; } export type DbJobDataWithUser = { @@ -83,6 +84,10 @@ export type DbUserDeleteJobData = { onlyFiles?: boolean; }; +export type DbUserTruncateJobData = { + user: ThinUser; +}; + export type DbUserImportJobData = { user: ThinUser; fileId: MiDriveFile['id']; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 7b9d59f99..10935ff5b 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -222,6 +222,7 @@ import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; +import * as ep___i_truncateAccount from './endpoints/i/truncate-account.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; @@ -616,6 +617,7 @@ const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default }; const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default }; const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default }; +const $i_truncateAccount: Provider = { provide: 'ep:i/truncate-account', useClass: ep___i_truncateAccount.default }; const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default }; const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; @@ -1014,6 +1016,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $i_claimAchievement, $i_changePassword, $i_deleteAccount, + $i_truncateAccount, $i_exportBlocking, $i_exportFollowing, $i_exportMute, @@ -1406,6 +1409,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $i_claimAchievement, $i_changePassword, $i_deleteAccount, + $i_truncateAccount, $i_exportBlocking, $i_exportFollowing, $i_exportMute, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 34a5cbc3a..33d59bfed 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -222,6 +222,7 @@ import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; +import * as ep___i_truncateAccount from './endpoints/i/truncate-account.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; @@ -614,6 +615,7 @@ const eps = [ ['i/claim-achievement', ep___i_claimAchievement], ['i/change-password', ep___i_changePassword], ['i/delete-account', ep___i_deleteAccount], + ['i/truncate-account', ep___i_truncateAccount], ['i/export-blocking', ep___i_exportBlocking], ['i/export-following', ep___i_exportFollowing], ['i/export-mute', ep___i_exportMute], diff --git a/packages/backend/src/server/api/endpoints/i/truncate-account.ts b/packages/backend/src/server/api/endpoints/i/truncate-account.ts new file mode 100644 index 000000000..8ff841789 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/truncate-account.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import bcrypt from 'bcryptjs'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { TruncateAccountService } from '@/core/TruncateAccountService.js'; +import { DI } from '@/di-symbols.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; + +export const meta = { + requireCredential: true, + + secure: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + password: { type: 'string' }, + token: { type: 'string', nullable: true }, + }, + required: ['password'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userAuthService: UserAuthService, + private truncateAccountService: TruncateAccountService, + ) { + super(meta, paramDef, async (ps, me) => { + const token = ps.token; + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } + + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); + if (userDetailed.isDeleted) { + return; + } + + const passwordMatched = await bcrypt.compare(ps.password, profile.password!); + if (!passwordMatched) { + throw new Error('incorrect password'); + } + + await this.truncateAccountService.truncateAccount(me); + }); + } +} diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 0b29ffa96..c5f1acac4 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -67,13 +67,24 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts._accountDelete.mayTakeTime }} - {{ i18n.ts._accountDelete.sendEmail }} + {{ i18n.ts._accountDelete.mayTakeTime }} + {{ i18n.ts._accountDelete.sendEmail }} {{ i18n.ts._accountDelete.requestAccountDelete }} {{ i18n.ts._accountDelete.inProgress }}
- {{ i18n.ts._accountDelete.youCantUseThisTime }} + {{ i18n.ts._accountDelete.youCantUseThisTime }} +
+ + + + + + +
+ {{ i18n.ts._accountTruncate.mayTakeTime }} + {{ i18n.ts._accountTruncate.requestAccountTruncate }} + {{ i18n.ts._accountTruncate.inProgress }}
@@ -105,11 +116,13 @@ SPDX-License-Identifier: AGPL-3.0-only