TwoFactorManagerbeta
Self-service two-factor (TOTP) management: enrol with an authenticator app, show one-time backup codes, and disable with a password re-auth. Pass a `qr` snippet to render the setup QR code; the package ships no QR encoder, so without one the otpauth URI and Base32 secret are shown as text to type in.
01 Usage
Basic
qr snippet is passed here, so the page falls back to manual entry. The code below 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 role="alert" region sits directly under the heading,
above the idle/setup/backup branch rather than inside it. An error raised while confirming a
code is therefore still announced after the view changes; a region nested in the branch would
unmount with the step before the reader heard it.
The secret and the backup codes are text elements
The TOTP secret renders in a <code> element
and the backup codes in a <ul> of <li>. A screen reader announces the list with
its item count and steps through the codes one at a time, where the same content in a grid
of styled 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.
Advancing a step moves focus to its heading
Moving from idle to setup to backup codes swaps the content in place, so the button that
was pressed is gone. Each step opens with an <h3> carrying tabindex="-1", and focus lands there once the new
step has rendered — the reader hears "Save your backup codes" instead of falling to <body>. Turning 2FA off does the same: the
disable form is replaced by the enable button, and focus goes to the panel heading, which
is also where a cancelled setup returns. A refused code changes no step, so nothing moves
and the caret stays in the field being corrected. The QR code is your snippet, so its
alternative text is yours to provide; the secret sits next to it as text 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 nothing renders.
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. Useful for mock backends in demos/tests or custom retry/auth layers. | |
onDisabled | () => void | — | Called after 2FA was disabled. | |
onEnabled | () => void | — | Called after 2FA was successfully enabled (e.g. refresh your auth store). | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ TwoFactorManager: { … } }}>.
Resolves after the provider defaults and before this instance's own
slotClasses, so a project-wide look lives in one place instead of being
repeated at every usage site. | |
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 and secret are
shown for manual entry. | |
slotClasses | Partial<Record<'root' | 'title' | 'section' | 'sectionTitle' | 'field' | 'submit' | 'error' | 'code' | 'backupCode', string>> | — | Per-slot class overrides. | |
t | PartialAuthLocale | — | Locale overrides, deep-merged over the bundle registered for the active
locale — English unless registerAuthLocale ran for it. Pass any subset,
from a single string to 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 bundle registered
for the active locale — English unless registerAuthLocale ran for it — 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 by mergeAuthLocale over the
bundle registered for the active locale — English unless registerAuthLocale
ran for it — so component markup reads keys directly, without per-key
?? '…' fallback literals.
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, so {{…}} here would render verbatim. |
05 Installation
Import
import { TwoFactorManager } from '@urbicon-ui/auth';