Files
HealthLog/src/app/api/settings/account/route.ts
T
Marc-André Bombeck b271a39fe4 merge: remove the bug-report integration for v1.23
# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	src/app/admin/[section]/renderer.tsx
2026-06-27 20:47:24 +02:00

86 lines
2.9 KiB
TypeScript

import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { destroyAllSessions } from "@/lib/auth/session";
import { NextRequest } from "next/server";
import {
apiHandler,
requireFreshMfaIfEnrolled,
MFA_STEP_UP_MAX_AGE_SECONDS,
} from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
export const dynamic = "force-dynamic";
/**
* Permanently delete the user account and ALL associated data.
* Cascading deletes in the schema handle all related records.
*
* v1.23 — for an account with a second factor active this requires a fresh
* step-up (within MFA_STEP_UP_MAX_AGE_SECONDS) in addition to the typed
* confirmation, so a hijacked live session cannot erase the record. Accounts
* without MFA are unaffected and keep the typed-confirmation-only contract.
*/
export const DELETE = apiHandler(async (request: NextRequest) => {
const { user } = await requireFreshMfaIfEnrolled(MFA_STEP_UP_MAX_AGE_SECONDS);
let confirm = "";
try {
const raw = await request.text();
if (raw.length > 64 * 1024) {
return apiError(`Request body exceeds ${64 * 1024} bytes`, 413);
}
const body = JSON.parse(raw);
confirm = typeof body?.confirm === "string" ? body.confirm : "";
} catch {
return apiError("Invalid request", 422);
}
if (confirm !== "DELETE_ACCOUNT") {
return apiError(
"Confirmation missing. Send { confirm: 'DELETE_ACCOUNT' }",
422,
);
}
// Prevent last admin from deleting themselves
if (user.role === "ADMIN") {
const adminCount = await prisma.user.count({ where: { role: "ADMIN" } });
if (adminCount <= 1) {
return apiError("The last admin account cannot be deleted", 400);
}
}
const userId = user.id;
const username = user.username;
// Log BEFORE deletion. The audit row's userId is SetNull post-cascade
// (per schema), but we then immediately purge it below for GDPR Art. 17
// erasure completeness — see comment further down.
await auditLog("user.account.delete", {
userId,
ipAddress: getClientIp(request),
details: { username },
});
// Destroy all sessions first
await destroyAllSessions(userId);
// Audit V3 NEW-V3-2 / GDPR Art. 17 fix: AuditLog rows have
// `onDelete: SetNull` in the schema, which keeps PII (IP addresses, login
// city geo) attached to the record after the user is deleted. We explicitly
// purge them inside the same logical operation so account deletion is
// genuinely complete erasure.
await prisma.auditLog.deleteMany({ where: { userId } });
// Delete user — all other related data is removed via onDelete: Cascade
await prisma.user.delete({ where: { id: userId } });
annotate({
action: { name: "settings.account.delete" },
meta: { username },
});
return apiSuccess({ deleted: true });
});