mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
# 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
86 lines
2.9 KiB
TypeScript
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 });
|
|
});
|