import { Injectable } from '@nestjs/common'; import { DELETED_USER_DISPLAY_NAME } from '@dorfteich/shared'; import { PinoLogger } from 'nestjs-pino'; import { PrismaService } from '../prisma/prisma.service'; /** * GDPR account deletion (issue #59, security.md §Privacy). Rather than * hard-deleting the user row — which would orphan or cascade authored content — * this scrubs the personal data, drops the login credentials, and trashes the * personal pond. The (kept) row is what `created_by`/authorship references, so * shared content the user authored simply shows as "Deleted user". */ @Injectable() export class PseudonymizationService { constructor( private readonly prisma: PrismaService, private readonly logger: PinoLogger, ) { this.logger.setContext(PseudonymizationService.name); } async pseudonymize(userId: string): Promise { const marker = `deleted-${userId}`; await this.prisma.$transaction(async (tx) => { // Remove every login path (password + any linked identities). await tx.userIdentity.deleteMany({ where: { userId } }); await tx.session.deleteMany({ where: { userId } }); await tx.user.update({ where: { id: userId }, data: { username: marker, email: `${marker}@deleted.invalid`, displayName: DELETED_USER_DISPLAY_NAME, status: 'DISABLED', isSiteAdmin: false, emailVerifiedAt: null, }, }); // The personal pond follows the trash path (security.md §Privacy). await tx.pond.updateMany({ where: { ownerId: userId, type: 'PERSONAL', deletedAt: null }, data: { deletedAt: new Date(), deletedBy: userId }, }); }); this.logger.info({ userId }, 'audit: user pseudonymized (account deleted)'); } }