mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
Second-factor management has been cookie-only since v1.23. The gate is right: a Bearer token is a credential whose provenance the server cannot see, and tearing down a second factor must not be reachable from the weaker of an interactive login and a value pasted into a script. What was missing was never Bearer access to those routes — it was a way to say, over a transport with no session row, that the human holding this device just re-proved a factor. An elevation is that proof. It is minted only against a re-proved account password or primary passkey, bound to the exact token that re-proved, single-use via a conditional UPDATE, five-minute-lived to match the cookie path's step-up window, and stored hashed. A stolen token mints nothing; a second token cannot redeem another's elevation; a password change drops every one the account holds. requireMfaManagementAuth is the one gate the management routes share. On a cookie session it delegates to the original helpers unchanged, so the web flow runs the code it always did. requireAdmin stays cookie-only and out of reach.
42 lines
1.5 KiB
TypeScript
42 lines
1.5 KiB
TypeScript
/**
|
|
* GET /api/auth/me/mfa
|
|
*
|
|
* Second-factor status for the security settings hub: whether TOTP is active,
|
|
* how many recovery codes remain, and the registered WebAuthn security keys.
|
|
* Gated by `requireMfaManagementAuth`: a cookie session, or a Bearer token
|
|
* presenting a single-use step-up elevation. A token alone is not enough.
|
|
*
|
|
* Returns only metadata: no secret, no recovery-code plaintext, no public key.
|
|
*/
|
|
import { apiHandler, requireMfaManagementAuth } from "@/lib/api-handler";
|
|
import { apiSuccess } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { prisma } from "@/lib/db";
|
|
import { countRemainingRecoveryCodes } from "@/lib/auth/mfa/recovery-codes";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export const GET = apiHandler(async () => {
|
|
const { user } = await requireMfaManagementAuth();
|
|
|
|
const [recoveryCodesRemaining, webauthn] = await Promise.all([
|
|
countRemainingRecoveryCodes(user.id),
|
|
prisma.webauthnMfaCredential.findMany({
|
|
where: { userId: user.id },
|
|
select: { id: true, name: true, createdAt: true, lastUsedAt: true },
|
|
orderBy: { createdAt: "desc" },
|
|
}),
|
|
]);
|
|
|
|
annotate({ action: { name: "auth.mfa.status" } });
|
|
|
|
return apiSuccess({
|
|
totp: { enabled: Boolean(user.totpConfirmedAt) },
|
|
recoveryCodesRemaining,
|
|
webauthn,
|
|
// The security hub also drives the passkey-upgrade nudge from this one
|
|
// status read, so it knows whether the user has already dismissed it.
|
|
passkeyNudgeDismissed: Boolean(user.passkeyUpgradeNudgeDismissed),
|
|
});
|
|
});
|