mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +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.
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
/**
|
|
* Shared WebAuthn relying-party configuration.
|
|
*
|
|
* Both the passwordless-primary passkey path (`src/lib/auth/passkey.ts`) and
|
|
* the second-factor security-key path (`src/lib/auth/mfa/webauthn.ts`) resolve
|
|
* the same relying-party id / origin from the deployment's configured app URL,
|
|
* so the two ceremonies always agree on the RP they bind credentials to.
|
|
*
|
|
* The candidates mirror the rest of the app: `APP_URL` first, then the public
|
|
* build-time URL, then a localhost fallback for dev. A multi-origin deployment
|
|
* (rare for a single-tenant self-host, but supported) passes the whole set to
|
|
* SimpleWebAuthn's `expectedOrigin`.
|
|
*/
|
|
|
|
/** Human-readable relying-party name shown by the authenticator UI. */
|
|
export const RP_NAME = "HealthLog";
|
|
|
|
export function getConfiguredOrigins(): string[] {
|
|
const candidates = [
|
|
process.env.APP_URL,
|
|
process.env.NEXT_PUBLIC_APP_URL,
|
|
"http://localhost:3000",
|
|
]
|
|
.map((value) => value?.trim())
|
|
.filter((value): value is string => Boolean(value));
|
|
|
|
const validOrigins = candidates
|
|
.map((value) => {
|
|
try {
|
|
return new URL(value).origin;
|
|
} catch {
|
|
return null;
|
|
}
|
|
})
|
|
.filter((value): value is string => Boolean(value));
|
|
|
|
return Array.from(new Set(validOrigins));
|
|
}
|
|
|
|
export function getRpId(): string {
|
|
const origins = getConfiguredOrigins();
|
|
return new URL(origins[0]).hostname;
|
|
}
|
|
|
|
export function getExpectedOrigin(): string | string[] {
|
|
const origins = getConfiguredOrigins();
|
|
return origins.length === 1 ? origins[0] : origins;
|
|
}
|