mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 13:41:52 +00:00
fix(server): handle hanging workspace after user account deleted (#8377)
fix CLOUD-74
This commit is contained in:
@@ -132,11 +132,6 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
async deleteSpace(workspaceId: string) {
|
async deleteSpace(workspaceId: string) {
|
||||||
const ident = { where: { workspaceId } };
|
const ident = { where: { workspaceId } };
|
||||||
await this.db.$transaction([
|
await this.db.$transaction([
|
||||||
this.db.workspace.deleteMany({
|
|
||||||
where: {
|
|
||||||
id: workspaceId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.db.snapshot.deleteMany(ident),
|
this.db.snapshot.deleteMany(ident),
|
||||||
this.db.update.deleteMany(ident),
|
this.db.update.deleteMany(ident),
|
||||||
this.db.snapshotHistory.deleteMany(ident),
|
this.db.snapshotHistory.deleteMany(ident),
|
||||||
@@ -344,6 +339,17 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const historyMaxAge = await this.options
|
||||||
|
.historyMaxAge(snapshot.spaceId)
|
||||||
|
.catch(
|
||||||
|
() =>
|
||||||
|
0 /* edgecase: user deleted but owned workspaces not handled correctly */
|
||||||
|
);
|
||||||
|
|
||||||
|
if (historyMaxAge === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await this.db.snapshotHistory
|
await this.db.snapshotHistory
|
||||||
.create({
|
.create({
|
||||||
select: {
|
select: {
|
||||||
@@ -355,9 +361,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
timestamp: new Date(snapshot.timestamp),
|
timestamp: new Date(snapshot.timestamp),
|
||||||
blob: Buffer.from(snapshot.bin),
|
blob: Buffer.from(snapshot.bin),
|
||||||
createdBy: snapshot.editor,
|
createdBy: snapshot.editor,
|
||||||
expiredAt: new Date(
|
expiredAt: new Date(Date.now() + historyMaxAge),
|
||||||
Date.now() + (await this.options.historyMaxAge(snapshot.spaceId))
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
|
|||||||
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { CallTimer, Config, metrics } from '../../fundamentals';
|
import {
|
||||||
|
CallTimer,
|
||||||
|
Config,
|
||||||
|
type EventPayload,
|
||||||
|
metrics,
|
||||||
|
OnEvent,
|
||||||
|
} from '../../fundamentals';
|
||||||
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
|
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -73,4 +79,11 @@ export class DocStorageCronJob implements OnModuleInit {
|
|||||||
.gauge('updates_queue_count')
|
.gauge('updates_queue_count')
|
||||||
.record(await this.db.update.count());
|
.record(await this.db.update.count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.deleted')
|
||||||
|
async clearUserWorkspaces(payload: EventPayload<'user.deleted'>) {
|
||||||
|
for (const workspace of payload.ownedWorkspaces) {
|
||||||
|
await this.workspace.deleteSpace(workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PermissionModule } from '../permission';
|
||||||
import { StorageModule } from '../storage';
|
import { StorageModule } from '../storage';
|
||||||
import { UserAvatarController } from './controller';
|
import { UserAvatarController } from './controller';
|
||||||
import { UserManagementResolver, UserResolver } from './resolver';
|
import { UserManagementResolver, UserResolver } from './resolver';
|
||||||
import { UserService } from './service';
|
import { UserService } from './service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StorageModule],
|
imports: [StorageModule, PermissionModule],
|
||||||
providers: [UserResolver, UserService, UserManagementResolver],
|
providers: [UserResolver, UserService, UserManagementResolver],
|
||||||
controllers: [UserAvatarController],
|
controllers: [UserAvatarController],
|
||||||
exports: [UserService],
|
exports: [UserService],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
WrongSignInCredentials,
|
WrongSignInCredentials,
|
||||||
WrongSignInMethod,
|
WrongSignInMethod,
|
||||||
} from '../../fundamentals';
|
} from '../../fundamentals';
|
||||||
|
import { PermissionService } from '../permission';
|
||||||
import { Quota_FreePlanV1_1 } from '../quota/schema';
|
import { Quota_FreePlanV1_1 } from '../quota/schema';
|
||||||
import { validators } from '../utils/validators';
|
import { validators } from '../utils/validators';
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ export class UserService {
|
|||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private readonly crypto: CryptoHelper,
|
private readonly crypto: CryptoHelper,
|
||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly emitter: EventEmitter
|
private readonly emitter: EventEmitter,
|
||||||
|
private readonly permission: PermissionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get userCreatingData() {
|
get userCreatingData() {
|
||||||
@@ -276,12 +278,13 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(id: string) {
|
async deleteUser(id: string) {
|
||||||
|
const ownedWorkspaces = await this.permission.getOwnedWorkspaces(id);
|
||||||
const user = await this.prisma.user.delete({ where: { id } });
|
const user = await this.prisma.user.delete({ where: { id } });
|
||||||
this.emitter.emit('user.deleted', user);
|
this.emitter.emit('user.deleted', { ...user, ownedWorkspaces });
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('user.updated')
|
@OnEvent('user.updated')
|
||||||
async onUserUpdated(user: EventPayload<'user.deleted'>) {
|
async onUserUpdated(user: EventPayload<'user.updated'>) {
|
||||||
const { enabled, customerIo } = this.config.metrics;
|
const { enabled, customerIo } = this.config.metrics;
|
||||||
if (enabled && customerIo?.token) {
|
if (enabled && customerIo?.token) {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
UserNotFound,
|
UserNotFound,
|
||||||
} from '../../../fundamentals';
|
} from '../../../fundamentals';
|
||||||
import { CurrentUser, Public } from '../../auth';
|
import { CurrentUser, Public } from '../../auth';
|
||||||
import type { Editor } from '../../doc';
|
import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc';
|
||||||
import { DocContentService } from '../../doc-renderer';
|
import { DocContentService } from '../../doc-renderer';
|
||||||
import { Permission, PermissionService } from '../../permission';
|
import { Permission, PermissionService } from '../../permission';
|
||||||
import { QuotaManagementService, QuotaQueryType } from '../../quota';
|
import { QuotaManagementService, QuotaQueryType } from '../../quota';
|
||||||
@@ -86,7 +86,8 @@ export class WorkspaceResolver {
|
|||||||
private readonly event: EventEmitter,
|
private readonly event: EventEmitter,
|
||||||
private readonly blobStorage: WorkspaceBlobStorage,
|
private readonly blobStorage: WorkspaceBlobStorage,
|
||||||
private readonly mutex: RequestMutex,
|
private readonly mutex: RequestMutex,
|
||||||
private readonly doc: DocContentService
|
private readonly doc: DocContentService,
|
||||||
|
private readonly workspaceStorage: PgWorkspaceDocStorageAdapter
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ResolveField(() => Permission, {
|
@ResolveField(() => Permission, {
|
||||||
@@ -352,6 +353,7 @@ export class WorkspaceResolver {
|
|||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await this.workspaceStorage.deleteSpace(id);
|
||||||
|
|
||||||
this.event.emit('workspace.deleted', id);
|
this.event.emit('workspace.deleted', id);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ export interface DocEvents {
|
|||||||
|
|
||||||
export interface UserEvents {
|
export interface UserEvents {
|
||||||
updated: Payload<Omit<User, 'password'>>;
|
updated: Payload<Omit<User, 'password'>>;
|
||||||
deleted: Payload<User>;
|
deleted: Payload<
|
||||||
|
User & {
|
||||||
|
ownedWorkspaces: Workspace['id'][];
|
||||||
|
}
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user