refactor(account): account delete, truncate, auto note removal
This commit is contained in:
parent
4e99008e8d
commit
d84bff37ec
17 changed files with 387 additions and 260 deletions
|
@ -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"`);
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}, {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue