fix(server): handle hanging workspace after user account deleted (#8377)

fix CLOUD-74
This commit is contained in:
forehalo
2024-09-25 06:16:32 +00:00
parent d0050a268a
commit 1d75d97a8f
6 changed files with 43 additions and 16 deletions

View File

@@ -132,11 +132,6 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
async deleteSpace(workspaceId: string) {
const ident = { where: { workspaceId } };
await this.db.$transaction([
this.db.workspace.deleteMany({
where: {
id: workspaceId,
},
}),
this.db.snapshot.deleteMany(ident),
this.db.update.deleteMany(ident),
this.db.snapshotHistory.deleteMany(ident),
@@ -344,6 +339,17 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
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
.create({
select: {
@@ -355,9 +361,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
timestamp: new Date(snapshot.timestamp),
blob: Buffer.from(snapshot.bin),
createdBy: snapshot.editor,
expiredAt: new Date(
Date.now() + (await this.options.historyMaxAge(snapshot.spaceId))
),
expiredAt: new Date(Date.now() + historyMaxAge),
},
})
.catch(() => {

View File

@@ -2,7 +2,13 @@ import { Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
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';
@Injectable()
@@ -73,4 +79,11 @@ export class DocStorageCronJob implements OnModuleInit {
.gauge('updates_queue_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);
}
}
}

View File

@@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
import { PermissionModule } from '../permission';
import { StorageModule } from '../storage';
import { UserAvatarController } from './controller';
import { UserManagementResolver, UserResolver } from './resolver';
import { UserService } from './service';
@Module({
imports: [StorageModule],
imports: [StorageModule, PermissionModule],
providers: [UserResolver, UserService, UserManagementResolver],
controllers: [UserAvatarController],
exports: [UserService],

View File

@@ -11,6 +11,7 @@ import {
WrongSignInCredentials,
WrongSignInMethod,
} from '../../fundamentals';
import { PermissionService } from '../permission';
import { Quota_FreePlanV1_1 } from '../quota/schema';
import { validators } from '../utils/validators';
@@ -34,7 +35,8 @@ export class UserService {
private readonly config: Config,
private readonly crypto: CryptoHelper,
private readonly prisma: PrismaClient,
private readonly emitter: EventEmitter
private readonly emitter: EventEmitter,
private readonly permission: PermissionService
) {}
get userCreatingData() {
@@ -276,12 +278,13 @@ export class UserService {
}
async deleteUser(id: string) {
const ownedWorkspaces = await this.permission.getOwnedWorkspaces(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')
async onUserUpdated(user: EventPayload<'user.deleted'>) {
async onUserUpdated(user: EventPayload<'user.updated'>) {
const { enabled, customerIo } = this.config.metrics;
if (enabled && customerIo?.token) {
const payload = {

View File

@@ -30,7 +30,7 @@ import {
UserNotFound,
} from '../../../fundamentals';
import { CurrentUser, Public } from '../../auth';
import type { Editor } from '../../doc';
import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc';
import { DocContentService } from '../../doc-renderer';
import { Permission, PermissionService } from '../../permission';
import { QuotaManagementService, QuotaQueryType } from '../../quota';
@@ -86,7 +86,8 @@ export class WorkspaceResolver {
private readonly event: EventEmitter,
private readonly blobStorage: WorkspaceBlobStorage,
private readonly mutex: RequestMutex,
private readonly doc: DocContentService
private readonly doc: DocContentService,
private readonly workspaceStorage: PgWorkspaceDocStorageAdapter
) {}
@ResolveField(() => Permission, {
@@ -352,6 +353,7 @@ export class WorkspaceResolver {
id,
},
});
await this.workspaceStorage.deleteSpace(id);
this.event.emit('workspace.deleted', id);

View File

@@ -19,7 +19,11 @@ export interface DocEvents {
export interface UserEvents {
updated: Payload<Omit<User, 'password'>>;
deleted: Payload<User>;
deleted: Payload<
User & {
ownedWorkspaces: Workspace['id'][];
}
>;
}
/**