TwoFactorManager
Self-service two-factor (TOTP) management: enrol with an authenticator app, show one-time backup codes, and disable with a password re-auth. The core stays zero-dependency, so QR rendering is delegated to the `qr` snippet — without it the otpauth URI + Base32 secret are shown for manual entry.
01 Usage
Basic
qr snippet is omitted here, so the zero-dep manual-entry fallback is shown. The snippet shows the production setup.Two-factor authentication
Add a second step to sign-in using an authenticator app.
<script lang="ts">
import { TwoFactorManager } from '@urbicon-ui/auth';
import type { AuthUser } from '@urbicon-ui/auth';
// `user` typically comes from your auth store or `locals.user`.
let { user }: { user: AuthUser | null } = $props();
</script>
<TwoFactorManager {user} apiPath="/api/auth/account/2fa" />
02 Accessibility
The error region outlives the step it came from
The aria-live="polite" region sits directly under the
heading, above the idle/setup/backup branch — not inside it. An error raised while confirming
a code is therefore still announced after the view changes, which a region nested in the branch
would have destroyed before the reader got to it.
The secret and the backup codes are real elements
The TOTP secret renders in a <code> element
and the backup codes in a <ul> of <li>. A reader announces the list with its
item count and can step through the codes one at a time — the same content as a styled
grid of divs would be an unnavigable run of characters.
Autofill hints on both entry paths
Disabling 2FA re-authenticates with an autoComplete="current-password" field; the setup
code uses inputmode="numeric" with autoComplete="one-time-code", which brings up the
numeric keypad and lets the OS offer the code directly.
Step changes are neither focused nor announced
Moving from idle to setup to backup codes replaces the content in place, and nothing marks it: focus is not moved, and the live region above carries only errors — a successful step change clears the error first, so the region is empty exactly when the view swaps. A screen-reader user is left on a page whose content silently became something else. Move focus to the new step yourself if this flow matters to you. The QR code is a consumer-supplied snippet, so its alternative text is yours to provide; the secret is always available as text next to it for anyone who cannot scan.
03 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
user required | AuthUser | null | — | The current authenticated user — its totpEnabled seeds the initial state
and its email labels the otpauth entry. While null the panel renders
nothing. Resolve user before mount, or remount with
{#key user?.id}…{/key} to re-seed after an async load. | |
apiPath | string | '/api/auth/account/2fa' | API base path for the 2FA account endpoints. | |
class | string | — | Extra classes on the root element. | |
csrf | CsrfClientOptions | — | CSRF cookie/header names — only needed when the server overrides the defaults via config.csrf. Mutating requests echo the token automatically. | |
fetcher | typeof globalThis.fetch | — | Custom fetch implementation for all API calls. Defaults to the global fetch. | |
onDisabled | () => void | — | Called after 2FA was disabled. | |
onEnabled | () => void | — | Called after 2FA was successfully enabled (e.g. refresh your auth store). | |
qr | Snippet<[{ uri: string; secret: string }]> | — | QR-code renderer for the otpauth URI shown during setup. Receives the
otpauth:// uri and the Base32 secret. Optional — the package ships no
QR encoder (zero-dep), so without this snippet only the URI + secret are
shown for manual entry. | |
slotClasses | Partial<Record<'root' | 'title' | 'section' | 'sectionTitle' | 'field' | 'submit' | 'code' | 'backupCode', string>> | — | Per-slot class overrides. | |
t | PartialAuthLocale | — | Locale overrides, deep-merged over the active built-in bundle (resolved from the i18n context). Pass any subset — a single string or a whole tree. | |
unstyled | boolean | — | Strip all default styling. |
04 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
TwoFactorManagerProps | interface | props | 0 | — | |
PartialAuthLocale | type | helper | 1 | Consumer-facing locale input: any subset of AuthLocale. Components
accept this as their t prop and deep-merge it over the active built-in
bundle, so overriding a single string never silently blanks the rest. | |
AuthUser | interface | helper | 1 | — | |
CsrfClientOptions | interface | helper | 1 | — | |
DeepPartial | type | helper | 0 | Recursive partial: every branch and leaf becomes optional. | |
AuthLocale | interface | helper | 0 | The complete auth locale bundle. Every key is required: the bundles this
package ships (en, de) satisfy the full shape, and consumer overrides
enter as PartialAuthLocale — deep-merged over the active built-in
bundle by mergeAuthLocale — so component markup reads keys directly,
without per-key ?? '…' fallback literals (review R19).
Placeholder convention: dynamic values use **single-brace** tokens
({n}, {name}, {email}) that the consuming component substitutes itself
via String.replace('{token}', value). There is deliberately **no**
{{…}} runtime interpolator in this package — the key-based translator twin
(authT/at) was removed in R21 — so {{…}} here would render verbatim. |
05 Installation
Import
import { TwoFactorManager } from '@urbicon-ui/auth';