mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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>
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* Admin-only global wipe of all health/integration data.
|
|
* Keeps users/passkeys so access to the app remains possible.
|
|
*/
|
|
export const DELETE = apiHandler(async (request: NextRequest) => {
|
|
const ip = getClientIp(request);
|
|
const rl = await checkRateLimit(`admin-data-delete:${ip}`, 5, 60 * 1000);
|
|
if (!rl.allowed) {
|
|
return NextResponse.json(
|
|
{ data: null, error: "Rate limit exceeded" },
|
|
{ status: 429, headers: rateLimitHeaders(rl) },
|
|
);
|
|
}
|
|
|
|
const { user } = await requireAdmin();
|
|
annotate({ action: { name: "admin.data.delete" } });
|
|
|
|
let confirm = "";
|
|
try {
|
|
const body = await request.json();
|
|
confirm = typeof body?.confirm === "string" ? body.confirm : "";
|
|
} catch {
|
|
return apiError("Invalid request", 422);
|
|
}
|
|
|
|
if (confirm !== "DELETE ALL") {
|
|
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 authChallenges = await tx.authChallenge.deleteMany({});
|
|
|
|
await tx.user.updateMany({
|
|
data: {
|
|
heightCm: null,
|
|
dateOfBirth: null,
|
|
gender: null,
|
|
insightsPrivacyMode: "aggregated",
|
|
insightsCachedAt: null,
|
|
insightsCachedText: null,
|
|
telegramBotToken: null,
|
|
telegramChatId: null,
|
|
telegramEnabled: false,
|
|
withingsClientIdEncrypted: null,
|
|
withingsClientSecretEncrypted: null,
|
|
onboardingCompletedAt: null,
|
|
},
|
|
});
|
|
|
|
return {
|
|
measurements: measurements.count,
|
|
intakeEvents: intakeEvents.count,
|
|
medications: medications.count,
|
|
apiTokens: apiTokens.count,
|
|
withingsConnections: withingsConnections.count,
|
|
authChallenges: authChallenges.count,
|
|
};
|
|
});
|
|
|
|
await auditLog("admin.data.clear", {
|
|
userId: user.id,
|
|
ipAddress: getClientIp(request),
|
|
details: result,
|
|
});
|
|
|
|
return apiSuccess({ cleared: true, ...result });
|
|
});
|