enhance(server): Use job queue for account delete (#7668)
* enhance(server): Use job queue for account delete Fix #5336 * ジョブをひとつに * remove done call * clean up * add User.isDeleted * コミット忘れ * Update 1629512953000-user-is-deleted.ts * show dialog * lint * Update 1629512953000-user-is-deleted.ts
This commit is contained in:
		
							parent
							
								
									8ab9068d8e
								
							
						
					
					
						commit
						fd1ef4a62d
					
				
					 11 changed files with 135 additions and 3 deletions
				
			
		|  | @ -10,6 +10,7 @@ | ||||||
| ## 12.x.x (unreleased) | ## 12.x.x (unreleased) | ||||||
| 
 | 
 | ||||||
| ### Improvements | ### Improvements | ||||||
|  | - アカウント削除の安定性を向上 | ||||||
| - 絵文字オートコンプリートの挙動を改修 | - 絵文字オートコンプリートの挙動を改修 | ||||||
| - localStorageのaccountsはindexedDBで保持するように | - localStorageのaccountsはindexedDBで保持するように | ||||||
| - ActivityPub: ジョブキューの試行タイミングを調整 (#7635) | - ActivityPub: ジョブキューの試行タイミングを調整 (#7635) | ||||||
|  |  | ||||||
|  | @ -777,6 +777,7 @@ misskeyUpdated: "Misskeyが更新されました!" | ||||||
| whatIsNew: "更新情報を見る" | whatIsNew: "更新情報を見る" | ||||||
| translate: "翻訳" | translate: "翻訳" | ||||||
| translatedFrom: "{x}から翻訳" | translatedFrom: "{x}から翻訳" | ||||||
|  | accountDeletionInProgress: "アカウントの削除が進行中です" | ||||||
| 
 | 
 | ||||||
| _docs:  | _docs:  | ||||||
|   continueReading: "続きを読む" |   continueReading: "続きを読む" | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								migration/1629512953000-user-is-deleted.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								migration/1629512953000-user-is-deleted.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class isUserDeleted1629512953000 implements MigrationInterface { | ||||||
|  |     name = 'isUserDeleted1629512953000' | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" boolean NOT NULL DEFAULT false`); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS 'Whether the User is deleted.'`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeleted"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -11,6 +11,7 @@ type Account = { | ||||||
| 	token: string; | 	token: string; | ||||||
| 	isModerator: boolean; | 	isModerator: boolean; | ||||||
| 	isAdmin: boolean; | 	isAdmin: boolean; | ||||||
|  | 	isDeleted: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const data = localStorage.getItem('account'); | const data = localStorage.getItem('account'); | ||||||
|  |  | ||||||
|  | @ -310,6 +310,13 @@ for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| if ($i) { | if ($i) { | ||||||
|  | 	if ($i.isDeleted) { | ||||||
|  | 		dialog({ | ||||||
|  | 			type: 'warning', | ||||||
|  | 			text: i18n.locale.accountDeletionInProgress, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if ('Notification' in window) { | 	if ('Notification' in window) { | ||||||
| 		// 許可を得ていなかったらリクエスト
 | 		// 許可を得ていなかったらリクエスト
 | ||||||
| 		if (Notification.permission === 'default') { | 		if (Notification.permission === 'default') { | ||||||
|  |  | ||||||
|  | @ -175,6 +175,13 @@ export class User { | ||||||
| 	}) | 	}) | ||||||
| 	public isExplorable: boolean; | 	public isExplorable: boolean; | ||||||
| 
 | 
 | ||||||
|  | 	// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
 | ||||||
|  | 	@Column('boolean', { | ||||||
|  | 		default: false, | ||||||
|  | 		comment: 'Whether the User is deleted.' | ||||||
|  | 	}) | ||||||
|  | 	public isDeleted: boolean; | ||||||
|  | 
 | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 128, array: true, default: '{}' | 		length: 128, array: true, default: '{}' | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -252,6 +252,7 @@ export class UserRepository extends Repository<User> { | ||||||
| 				autoAcceptFollowed: profile!.autoAcceptFollowed, | 				autoAcceptFollowed: profile!.autoAcceptFollowed, | ||||||
| 				noCrawle: profile!.noCrawle, | 				noCrawle: profile!.noCrawle, | ||||||
| 				isExplorable: user.isExplorable, | 				isExplorable: user.isExplorable, | ||||||
|  | 				isDeleted: user.isDeleted, | ||||||
| 				hideOnlineStatus: user.hideOnlineStatus, | 				hideOnlineStatus: user.hideOnlineStatus, | ||||||
| 				hasUnreadSpecifiedNotes: NoteUnreads.count({ | 				hasUnreadSpecifiedNotes: NoteUnreads.count({ | ||||||
| 					where: { userId: user.id, isSpecified: true }, | 					where: { userId: user.id, isSpecified: true }, | ||||||
|  |  | ||||||
|  | @ -171,6 +171,15 @@ export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id'] | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function createDeleteAccountJob(user: ThinUser) { | ||||||
|  | 	return dbQueue.add('deleteAccount', { | ||||||
|  | 		user: user | ||||||
|  | 	}, { | ||||||
|  | 		removeOnComplete: true, | ||||||
|  | 		removeOnFail: true | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function createDeleteObjectStorageFileJob(key: string) { | export function createDeleteObjectStorageFileJob(key: string) { | ||||||
| 	return objectStorageQueue.add('deleteFile', { | 	return objectStorageQueue.add('deleteFile', { | ||||||
| 		key: key | 		key: key | ||||||
|  |  | ||||||
							
								
								
									
										79
									
								
								src/queue/processors/db/delete-account.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/queue/processors/db/delete-account.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | import * as Bull from 'bull'; | ||||||
|  | import { queueLogger } from '../../logger'; | ||||||
|  | import { DriveFiles, Notes, Users } from '@/models/index'; | ||||||
|  | import { DbUserJobData } from '@/queue/types'; | ||||||
|  | import { Note } from '@/models/entities/note'; | ||||||
|  | import { DriveFile } from '@/models/entities/drive-file'; | ||||||
|  | import { MoreThan } from 'typeorm'; | ||||||
|  | import { deleteFileSync } from '@/services/drive/delete-file'; | ||||||
|  | 
 | ||||||
|  | const logger = queueLogger.createSubLogger('delete-account'); | ||||||
|  | 
 | ||||||
|  | export async function deleteAccount(job: Bull.Job<DbUserJobData>): Promise<string | void> { | ||||||
|  | 	logger.info(`Deleting account of ${job.data.user.id} ...`); | ||||||
|  | 
 | ||||||
|  | 	const user = await Users.findOne(job.data.user.id); | ||||||
|  | 	if (user == null) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	{ // Delete notes
 | ||||||
|  | 		let cursor: Note['id'] | null = null; | ||||||
|  | 
 | ||||||
|  | 		while (true) { | ||||||
|  | 			const notes = await Notes.find({ | ||||||
|  | 				where: { | ||||||
|  | 					userId: user.id, | ||||||
|  | 					...(cursor ? { id: MoreThan(cursor) } : {}) | ||||||
|  | 				}, | ||||||
|  | 				take: 100, | ||||||
|  | 				order: { | ||||||
|  | 					id: 1 | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (notes.length === 0) { | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			cursor = notes[notes.length - 1].id; | ||||||
|  | 
 | ||||||
|  | 			await Notes.delete(notes.map(note => note.id)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		logger.succ(`All of notes deleted`); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	{ // Delete files
 | ||||||
|  | 		let cursor: DriveFile['id'] | null = null; | ||||||
|  | 
 | ||||||
|  | 		while (true) { | ||||||
|  | 			const files = await DriveFiles.find({ | ||||||
|  | 				where: { | ||||||
|  | 					userId: user.id, | ||||||
|  | 					...(cursor ? { id: MoreThan(cursor) } : {}) | ||||||
|  | 				}, | ||||||
|  | 				take: 10, | ||||||
|  | 				order: { | ||||||
|  | 					id: 1 | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (files.length === 0) { | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			cursor = files[files.length - 1].id; | ||||||
|  | 
 | ||||||
|  | 			for (const file of files) { | ||||||
|  | 				await deleteFileSync(file); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		logger.succ(`All of files deleted`); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	await Users.delete(job.data.user.id); | ||||||
|  | 
 | ||||||
|  | 	return 'Account deleted'; | ||||||
|  | } | ||||||
|  | @ -8,6 +8,7 @@ import { exportBlocking } from './export-blocking'; | ||||||
| import { exportUserLists } from './export-user-lists'; | import { exportUserLists } from './export-user-lists'; | ||||||
| import { importFollowing } from './import-following'; | import { importFollowing } from './import-following'; | ||||||
| import { importUserLists } from './import-user-lists'; | import { importUserLists } from './import-user-lists'; | ||||||
|  | import { deleteAccount } from './delete-account'; | ||||||
| 
 | 
 | ||||||
| const jobs = { | const jobs = { | ||||||
| 	deleteDriveFiles, | 	deleteDriveFiles, | ||||||
|  | @ -17,7 +18,8 @@ const jobs = { | ||||||
| 	exportBlocking, | 	exportBlocking, | ||||||
| 	exportUserLists, | 	exportUserLists, | ||||||
| 	importFollowing, | 	importFollowing, | ||||||
| 	importUserLists | 	importUserLists, | ||||||
|  | 	deleteAccount, | ||||||
| } as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>; | } as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>; | ||||||
| 
 | 
 | ||||||
| export default function(dbQueue: Bull.Queue<DbJobData>) { | export default function(dbQueue: Bull.Queue<DbJobData>) { | ||||||
|  |  | ||||||
|  | @ -1,9 +1,10 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import * as bcrypt from 'bcryptjs'; | import * as bcrypt from 'bcryptjs'; | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
| import { Users, UserProfiles } from '@/models/index'; | import { UserProfiles, Users } from '@/models/index'; | ||||||
| import { doPostSuspend } from '@/services/suspend-user'; | import { doPostSuspend } from '@/services/suspend-user'; | ||||||
| import { publishUserEvent } from '@/services/stream'; | import { publishUserEvent } from '@/services/stream'; | ||||||
|  | import { createDeleteAccountJob } from '@/queue'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	requireCredential: true as const, | 	requireCredential: true as const, | ||||||
|  | @ -19,6 +20,10 @@ export const meta = { | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user) => { | export default define(meta, async (ps, user) => { | ||||||
| 	const profile = await UserProfiles.findOneOrFail(user.id); | 	const profile = await UserProfiles.findOneOrFail(user.id); | ||||||
|  | 	const userDetailed = await Users.findOneOrFail(user.id); | ||||||
|  | 	if (userDetailed.isDeleted) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// Compare password
 | 	// Compare password
 | ||||||
| 	const same = await bcrypt.compare(ps.password, profile.password!); | 	const same = await bcrypt.compare(ps.password, profile.password!); | ||||||
|  | @ -30,7 +35,11 @@ export default define(meta, async (ps, user) => { | ||||||
| 	// 物理削除する前にDelete activityを送信する
 | 	// 物理削除する前にDelete activityを送信する
 | ||||||
| 	await doPostSuspend(user).catch(e => {}); | 	await doPostSuspend(user).catch(e => {}); | ||||||
| 
 | 
 | ||||||
| 	await Users.delete(user.id); | 	createDeleteAccountJob(user); | ||||||
|  | 
 | ||||||
|  | 	await Users.update(user.id, { | ||||||
|  | 		isDeleted: true, | ||||||
|  | 	}); | ||||||
| 
 | 
 | ||||||
| 	// Terminate streaming
 | 	// Terminate streaming
 | ||||||
| 	publishUserEvent(user.id, 'terminate', {}); | 	publishUserEvent(user.id, 'terminate', {}); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue