forked from FoundKeyGang/FoundKey
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…
Reference in a new issue