refactor(account): account delete, truncate, auto note removal

This commit is contained in:
무라쿠모 2024-09-07 22:32:45 +09:00
parent 4e99008e8d
commit d84bff37ec
No known key found for this signature in database
GPG key ID: 139D6573F92DA9F7
17 changed files with 387 additions and 260 deletions

View file

@ -2,18 +2,12 @@ export class AutoNoteRemoval1725706236633 {
name = 'AutoNoteRemoval1725706236633'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "auto_removal_condition" ("id" character varying(32) NOT NULL, "deleteAfter" bigint NOT NULL DEFAULT '7', "noPiningNotes" boolean NOT NULL DEFAULT true, "noSpecifiedNotes" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_e6a0b2b5bc8fbc0a07d7e34be7d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "auto_removal_condition" ("userId" character varying(32) NOT NULL, "deleteAfter" bigint NOT NULL DEFAULT '7', "noPiningNotes" boolean NOT NULL DEFAULT true, "noSpecifiedNotes" boolean NOT NULL DEFAULT true)`);
await queryRunner.query(`ALTER TABLE "user" ADD "autoRemoval" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "user"."autoRemoval" IS 'Whether the User is using note auto removal.'`);
await queryRunner.query(`ALTER TABLE "user" ADD "autoRemovalConditionId" character varying(32) NOT NULL`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "UQ_051c1df368299ee71088b7508b8" UNIQUE ("autoRemovalConditionId")`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_051c1df368299ee71088b7508b8" FOREIGN KEY ("autoRemovalConditionId") REFERENCES "auto_removal_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_051c1df368299ee71088b7508b8"`);
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "UQ_051c1df368299ee71088b7508b8"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "autoRemovalConditionId"`);
await queryRunner.query(`COMMENT ON COLUMN "user"."autoRemoval" IS 'Whether the User is using note auto removal.'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "autoRemoval"`);
await queryRunner.query(`DROP TABLE "auto_removal_condition"`);

View file

@ -40,6 +40,9 @@ export type RolePolicies = {
canCreateContent: boolean;
canUpdateContent: boolean;
canDeleteContent: boolean;
canUseAutoNoteRemoval: boolean;
canUseAccountRemoval: boolean;
canUseAccountTruncate: boolean;
canPurgeAccount: boolean;
canUpdateAvatar: boolean;
canUpdateBanner: boolean;
@ -69,7 +72,6 @@ export type RolePolicies = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
canUseAccountRemoval: boolean;
mutualLinkSectionLimit: number;
mutualLinkLimit: number;
};
@ -82,6 +84,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
canCreateContent: true,
canUpdateContent: true,
canDeleteContent: true,
canUseAutoNoteRemoval: true,
canUseAccountRemoval: true,
canUseAccountTruncate: true,
canPurgeAccount: true,
canUpdateAvatar: true,
canUpdateBanner: true,
@ -111,7 +116,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
canUseAccountRemoval: true,
mutualLinkSectionLimit: 1,
mutualLinkLimit: 3,
};
@ -397,7 +401,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
canUseAutoNoteRemoval: calc('canUseAutoNoteRemoval', vs => vs.some(v => v === true)),
canUseAccountRemoval: calc('canUseAccountRemoval', vs => vs.some(v => v === true)),
canUseAccountTruncate: calc('canUseAccountTruncate', vs => vs.some(v => v === true)),
canPurgeAccount: calc('canPurgeAccount', vs => vs.some(v => v === true)),
canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)),
canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)),

View file

@ -196,6 +196,18 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canUseAutoNoteRemoval: {
type: 'boolean',
optional: false, nullable: false,
},
canUseAccountRemoval: {
type: 'boolean',
optional: false, nullable: false,
},
canUseAccountTruncate: {
type: 'boolean',
optional: false, nullable: false,
},
canPurgeAccount: {
type: 'boolean',
optional: false, nullable: false,
@ -312,10 +324,6 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
canUseAccountRemoval: {
type: 'boolean',
optional: false, nullable: false,
},
mutualLinkSectionLimit: {
type: 'integer',
optional: false, nullable: false,

View file

@ -151,7 +151,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
case 'autoNoteRemoval': return this.autoNoteRemovalProcessorService.process();
case 'autoNoteRemoval': return this.autoNoteRemovalProcessorService.process(job);
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
}, {

View file

@ -12,6 +12,7 @@ import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { IdService } from '@/core/IdService.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@ -33,6 +34,7 @@ export class AutoNoteRemovalProcessorService {
private autoRemovalConditionRepository: AutoRemovalConditionRepository,
private idService: IdService,
private roleService: RoleService,
private noteDeleteService: NoteDeleteService,
private queueLoggerService: QueueLoggerService,
) {
@ -40,7 +42,7 @@ export class AutoNoteRemovalProcessorService {
}
@bindThis
public async process(): Promise<void> {
public async process(job: Bull.Job<Record<string, unknown>>): Promise<void> {
this.logger.info('Checking notes that to remove automatically...');
this.logger.info('Checking users that enabled note auto-removal');
const users = await this.usersRepository.find({ where: { autoRemoval: true } });
@ -51,6 +53,8 @@ export class AutoNoteRemovalProcessorService {
const now = Date.now();
for (const user of users) {
const policies = await this.roleService.getUserPolicies(user.id);
if (!policies.canUseAutoNoteRemoval) continue;
const autoRemovalCondition = await this.autoRemovalConditionRepository.findOneByOrFail({ userId: user.id });
const pinings: MiUserNotePining[] = await this.userNotePiningsRepository.findBy({ userId: user.id });
const piningNoteIds: string[] = pinings.map(pining => pining.noteId); // pining.note always undefined (bug?)
@ -65,11 +69,11 @@ export class AutoNoteRemovalProcessorService {
// Delete notes
let cursor: MiNote['id'] | null = null;
let condition: string[] = [];
if (autoRemovalCondition.noSpecifiedNotes === true) {
if (autoRemovalCondition.noSpecifiedNotes) {
condition = [...condition, ...specifiedNoteIds];
}
if (autoRemovalCondition.noPiningNotes === true) {
if (autoRemovalCondition.noPiningNotes) {
condition = [...condition, ...piningNoteIds];
}
@ -99,11 +103,13 @@ export class AutoNoteRemovalProcessorService {
const createdAt: number = this.idService.parse(note.id).date.getTime();
const delta: number = now - createdAt;
if (delta > deleteAfter) {
await Promise.bind(this.noteDeleteService.delete(user, note, false, user));
Promise.bind(this.noteDeleteService.delete(user, note, false, user));
}
}
await job.updateProgress(100 / users.length * users.indexOf(user));
}
await job.updateProgress(100);
this.logger.succ('All of auto-removable notes deleted');
}

View file

@ -17,13 +17,9 @@ export const meta = {
requireCredential: true,
secure: true,
requireRolePolicy: 'canUseAccountRemoval',
errors: {
removalDisabled: {
message: 'Account removal is disabled by your role.',
code: 'REMOVAL_DISABLED',
id: '453d954b-3d8b-4df0-a261-b26ec6660ea3',
},
authenticationFailed: {
message: 'Your password or 2FA token is invalid.',
code: 'AUTHENTICATION_FAILED',
@ -57,16 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userAuthService: UserAuthService,
private deleteAccountService: DeleteAccountService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
const policies = await this.roleService.getUserPolicies(me.id);
if (!policies.canUseAccountRemoval) {
throw new ApiError(meta.errors.removalDisabled);
}
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
if (userDetailed.isDeleted) {
throw new ApiError(meta.errors.alreadyRemoved);

View file

@ -10,11 +10,26 @@ 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';
import {ApiError} from "@/server/api/error.js";
export const meta = {
requireCredential: true,
secure: true,
requireRolePolicy: 'canUseAccountTruncate',
errors: {
authenticationFailed: {
message: 'Your password or 2FA token is invalid.',
code: 'AUTHENTICATION_FAILED',
id: 'ea791cff-63e7-4b2a-92fc-646ab641794e',
},
alreadyRemoved: {
message: 'Your account is removed.',
code: 'ACCOUNT_REMOVED',
id: '59b8f0e6-6eb2-4dc1-a080-1de3108416d0',
},
},
} as const;
export const paramDef = {
@ -40,30 +55,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private truncateAccountService: TruncateAccountService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const purgeDrive = ps.purgeDrive ? true : false;
const purgeDrive = !!ps.purgeDrive;
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;
throw new ApiError(meta.errors.alreadyRemoved);
}
const passwordMatched = await bcrypt.compare(ps.password, profile.password!);
if (!passwordMatched) {
throw new Error('incorrect password');
throw new ApiError(meta.errors.authenticationFailed);
}
if (profile.twoFactorEnabled) {
const token = ps.token;
if (token == null) {
throw new ApiError(meta.errors.authenticationFailed);
}
await this.userAuthService.twoFactorAuthenticate(profile, token);
}
await this.truncateAccountService.truncateAccount(me, purgeDrive);