mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
Introduce a dedicated /settings/security section that gathers every account-protection control in one place: TOTP enrolment (QR plus manual secret, confirm, recovery codes shown once with copy and download), WebAuthn security keys as a second factor (register, list with last-used, rename, step-up-gated removal), recovery-code count and regeneration, and primary passkey management. The second-factor WebAuthn path keeps its own credential store, separate from primary passkeys, and uses non-resident keys so an account can hold many. Relying-party resolution is shared with the passkey path. A status endpoint backs the hub; the step-up gate now recognises a registered security key as an active second factor so a WebAuthn-only account can still reach the gated actions.
41 lines
1.4 KiB
TypeScript
41 lines
1.4 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.
|
|
* Cookie-only (`requireCookieAuth`) — managing MFA is never a Bearer surface.
|
|
*
|
|
* Returns only metadata: no secret, no recovery-code plaintext, no public key.
|
|
*/
|
|
import { apiHandler, requireCookieAuth } 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 requireCookieAuth();
|
|
|
|
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),
|
|
});
|
|
});
|