mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(admin): preserve audit trail across the data wipe
Two related corrections to the "Wipe all data" admin action: - Stop deleting the audit log inside the transaction. The whole point of the audit trail is to outlive the data it documents, and this is the single feature where that property matters most. AuditLog rows now survive the wipe. - Audit-log the *intent* (`admin.data.clear.start`) before the transaction begins, so even if the wipe crashes mid-flight there is a record of who initiated it. - The i18n copy and the success toast both undersold the scope: they only mentioned measurements/medications/intakes, but the wipe also clears API tokens, Withings connections, and any open auth challenges. EN + DE strings now enumerate every cleared category and call out that user accounts AND the audit log are preserved. - Mutation result type widened so the toast can render the full count tuple. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
31959e436e
commit
c3ca8614a6
4 files changed
+22
-7
No files matched your search
+2
-2
@@ -1326,10 +1326,10 @@
|
||||
"deleteAllData": "Alle Daten global löschen",
|
||||
"deleteAllDescription": "Löscht alle Mess-, Medikamenten-, Integrations- und Protokoll-Daten aller Benutzer. Benutzerkonten bleiben bestehen.",
|
||||
"deleteAllConfirm": "Alle Daten wirklich löschen?",
|
||||
"deleteAllConfirmDescription": "Dieser Vorgang löscht alle Messwerte, Medikamente, Einnahmen und Integrationsdaten aller Benutzer unwiderruflich. Benutzerkonten bleiben erhalten.",
|
||||
"deleteAllConfirmDescription": "Dieser Vorgang löscht alle Messwerte, Medikamente, Einnahmen, API-Tokens, Withings-Verbindungen und offene Authentifizierungs-Challenges aller Benutzer unwiderruflich. Benutzerkonten und das Audit-Log bleiben erhalten, damit der Löschvorgang selbst nachvollziehbar bleibt.",
|
||||
"deleteButton": "Alle Daten löschen",
|
||||
"finalDelete": "Endgültig löschen",
|
||||
"deletedResult": "Gelöscht: {measurements} Messwerte, {medications} Medikamente, {intakeEvents} Einnahmen",
|
||||
"deletedResult": "Gelöscht: {measurements} Messwerte, {medications} Medikamente, {intakeEvents} Einnahmen, {apiTokens} API-Tokens, {withingsConnections} Withings-Verbindungen, {authChallenges} Auth-Challenges",
|
||||
"authRegister": "Registrierung",
|
||||
"authLogin": "Anmeldung",
|
||||
"authLoginPasskey": "Passkey-Anmeldung",
|
||||
|
||||
+2
-2
@@ -1326,10 +1326,10 @@
|
||||
"deleteAllData": "Delete all data globally",
|
||||
"deleteAllDescription": "Deletes all measurement, medication, integration, and log data for all users. User accounts will be preserved.",
|
||||
"deleteAllConfirm": "Really delete all data?",
|
||||
"deleteAllConfirmDescription": "This will permanently delete all measurements, medications, intake records, and integration data for all users. User accounts will be preserved.",
|
||||
"deleteAllConfirmDescription": "This will permanently delete all measurements, medications, intake events, API tokens, Withings connections, and any open authentication challenges for all users. User accounts and the audit log are preserved so the wipe itself remains traceable.",
|
||||
"deleteButton": "Delete all data",
|
||||
"finalDelete": "Delete permanently",
|
||||
"deletedResult": "Deleted: {measurements} measurements, {medications} medications, {intakeEvents} intakes",
|
||||
"deletedResult": "Deleted: {measurements} measurements, {medications} medications, {intakeEvents} intakes, {apiTokens} API tokens, {withingsConnections} Withings connections, {authChallenges} auth challenges",
|
||||
"authRegister": "Registration",
|
||||
"authLogin": "Login",
|
||||
"authLoginPasskey": "Passkey login",
|
||||
|
||||
@@ -37,13 +37,23 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
return apiError("Confirmation missing", 422);
|
||||
}
|
||||
|
||||
// Audit-log the *intent* before the transaction begins so the trail
|
||||
// describing "an admin is about to wipe data" survives even if the
|
||||
// operation crashes midway. AuditLog rows are intentionally NOT
|
||||
// deleted as part of the wipe — the entire point of an audit trail
|
||||
// is to outlive the data it documents.
|
||||
await auditLog("admin.data.clear.start", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(request),
|
||||
details: { confirm: "DELETE ALL" },
|
||||
});
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const measurements = await tx.measurement.deleteMany({});
|
||||
const intakeEvents = await tx.medicationIntakeEvent.deleteMany({});
|
||||
const medications = await tx.medication.deleteMany({});
|
||||
const apiTokens = await tx.apiToken.deleteMany({});
|
||||
const withingsConnections = await tx.withingsConnection.deleteMany({});
|
||||
const auditLogs = await tx.auditLog.deleteMany({});
|
||||
const authChallenges = await tx.authChallenge.deleteMany({});
|
||||
|
||||
await tx.user.updateMany({
|
||||
@@ -69,7 +79,6 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
medications: medications.count,
|
||||
apiTokens: apiTokens.count,
|
||||
withingsConnections: withingsConnections.count,
|
||||
auditLogs: auditLogs.count,
|
||||
authChallenges: authChallenges.count,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,6 +35,9 @@ export function DangerZoneSection({ id }: { id: string }) {
|
||||
measurements: number;
|
||||
intakeEvents: number;
|
||||
medications: number;
|
||||
apiTokens: number;
|
||||
withingsConnections: number;
|
||||
authChallenges: number;
|
||||
};
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
@@ -44,6 +47,9 @@ export function DangerZoneSection({ id }: { id: string }) {
|
||||
measurements: data.measurements,
|
||||
medications: data.medications,
|
||||
intakeEvents: data.intakeEvents,
|
||||
apiTokens: data.apiTokens,
|
||||
withingsConnections: data.withingsConnections,
|
||||
authChallenges: data.authChallenges,
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -107,7 +113,7 @@ export function DangerZoneSection({ id }: { id: string }) {
|
||||
</div>
|
||||
{wipeMsg && (
|
||||
<p
|
||||
className={`mt-2 text-sm ${wipeMsg.startsWith(t("admin.deletedResult", { measurements: "", medications: "", intakeEvents: "" }).split(":")[0]) ? "text-dracula-green" : "text-destructive"}`}
|
||||
className={`mt-2 text-sm ${wipeMsg.startsWith(t("admin.deletedResult", { measurements: "", medications: "", intakeEvents: "", apiTokens: "", withingsConnections: "", authChallenges: "" }).split(":")[0]) ? "text-dracula-green" : "text-destructive"}`}
|
||||
>
|
||||
{wipeMsg}
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user