Some checks failed
CD / Build and push images (push) Successful in 3m12s
CI / Lint, typecheck, test (push) Failing after 2m29s
CI / Auth e2e pack (push) Successful in 3m32s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 11s
Instance operators get basic user administration for support, abuse handling, and GDPR groundwork (security.md §Privacy). - api `admin/`: Site-Admin-gated `/admin/users` — a searchable, paginated list (username, e-mail, status, role, pond count, last login) plus lifecycle actions: disable/enable (a disabled user is logged out everywhere and login is refused with the distinct `account_disabled`), resend verification, delete, and grant/revoke Site Admin. Guards: you cannot act on your own account (`cannot_modify_self`) and the last Site Admin cannot be dropped (`last_site_admin`). Every action is audit-logged with the actor. - `PseudonymizationService`: account deletion scrubs the PII, removes all login identities + sessions, and trashes the personal pond — the kept row is what authorship references, so shared content the user authored shows as "Deleted user" (no orphaned/cascaded content). - web: the Admin area gains a 'Users' surface — search, pagination, and the actions (destructive ones behind an inline two-step confirm; self-actions hidden). New `users` i18n namespace (de+en). - tests: `user-admin.e2e.db.test.ts` (disable → logout + login blocked; delete → pseudonymized authorship + personal pond trashed + credentials gone; last Site Admin and self protected; Site-Admin gating); a non-destructive browser `admin-users` pack proving disable-in-UI blocks login and enable restores it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
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<void> {
|
|
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)');
|
|
}
|
|
}
|