mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Admin, coach, consent, chart-overlay, device registration, insights feedback and provider chain, ECG ingest, onboarding, search and the AI provider settings move to parseJsonBody with the same message, error code, status and wide-event action they answered with. A refusal whose annotate meta is computed from the issues, or puts its keys in another order than the helper would, keeps that annotate verbatim in onInvalid. The routes that still call safeJson do something between the read and the schema: an optimistic-concurrency token split off the body, a count cap answered first, a refunded quota slot, a conditional read, a non-Zod check, or the body handed to a service.
69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
/**
|
|
* POST /api/admin/encryption/key-backup/confirm { keyId, fingerprint }
|
|
*
|
|
* The admin states that the active encryption key is backed up. The body
|
|
* names the key the admin was shown; when that is no longer the active key
|
|
* (rotated, or the server restarted with another key since the page loaded)
|
|
* the answer is 409 `encryption.keyBackup.stale` and nothing is recorded.
|
|
* No step-up: confirming is a low-risk write that reveals nothing.
|
|
*/
|
|
import type { NextRequest } from "next/server";
|
|
import { z } from "zod/v4";
|
|
|
|
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
|
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
|
|
import { parseJsonBody } from "@/lib/api/body";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import {
|
|
confirmKeyBackup,
|
|
KEY_BACKUP_STALE_CODE,
|
|
} from "@/lib/crypto/key-backup";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const confirmSchema = z
|
|
.object({
|
|
keyId: z.string().min(1).max(32),
|
|
fingerprint: z.string().regex(/^[0-9a-f]{12}$/),
|
|
})
|
|
.strict();
|
|
|
|
export const POST = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAdmin();
|
|
annotate({ action: { name: "admin.encryption.keyBackup.confirm" } });
|
|
|
|
const parsed = await parseJsonBody(request, confirmSchema, {
|
|
maxBytes: 1024,
|
|
});
|
|
if (!parsed.ok) return parsed.error;
|
|
|
|
const result = await confirmKeyBackup({
|
|
keyId: parsed.data.keyId,
|
|
fingerprint: parsed.data.fingerprint,
|
|
userId: user.id,
|
|
});
|
|
if (!result.ok) {
|
|
annotate({ meta: { key_backup_stale: true } });
|
|
return apiError(
|
|
"The encryption key changed since this page was loaded. Reload and confirm the backup of the current key.",
|
|
409,
|
|
{
|
|
errorCode: KEY_BACKUP_STALE_CODE,
|
|
activeKeyId: result.activeKeyId,
|
|
fingerprint: result.fingerprint,
|
|
},
|
|
);
|
|
}
|
|
|
|
await auditLog("encryption.keyBackup.confirmed", {
|
|
userId: user.id,
|
|
ipAddress: getClientIp(request),
|
|
details: {
|
|
keyId: result.status.activeKeyId,
|
|
fingerprint: result.status.fingerprint,
|
|
},
|
|
});
|
|
return apiSuccess(result.status);
|
|
});
|