/** * Passwordless login and one-time TOTP enrollment screens. * * QR material, manual keys and recovery codes exist only in React memory and * are dropped as soon as their enrollment phase completes. PWA activation is * blocked while those values or partially completed forms are visible so an * update cannot erase information that the server will not reveal again. */ import { useMemo, useState } from 'react' import type { FormEvent, ReactNode } from 'react' import { api, copyToClipboard, errorMessage, json, normalizeEnrollment, normalizeRecoveryCodes, } from './api' import { PwaControls, authRoute, usePwaUpdateBlocker } from './pwa' import { LanguageSelect, useI18n } from './i18n' import type { TotpEnrollment } from './types' function AuthShell({ eyebrow, title, children, footer, }: { eyebrow: string title: string children: ReactNode footer?: ReactNode }) { const { t } = useI18n() return (

{eyebrow}

{title}

{children} {footer && }
) } function TotpQr({ enrollment }: { enrollment: TotpEnrollment }) { const { t } = useI18n() const source = useMemo(() => { if (enrollment.qrDataUrl) return enrollment.qrDataUrl if (enrollment.qrSvg) return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(enrollment.qrSvg)}` return undefined }, [enrollment.qrDataUrl, enrollment.qrSvg]) return (
{source ? ( {t('auth.totp_qr_alt')} ) : ( {t('auth.qr_unavailable')} )}

{t('auth.bind_authenticator')}

  1. {t('auth.totp_step_scan')}
  2. {t('auth.totp_step_manual')}
  3. {t('auth.totp_step_confirm')}
) } function RecoveryCodes({ codes, onContinue }: { codes: string[]; onContinue: () => void }) { const { t } = useI18n() const [copied, setCopied] = useState(false) const text = codes.join('\n') const download = () => { const blob = new Blob( [`${t('auth.recovery_file_title')}\n`, `${t('auth.recovery_file_warning')}\n\n`, text, '\n'], { type: 'text/plain;charset=utf-8' }, ) const url = URL.createObjectURL(blob) const anchor = document.createElement('a') anchor.href = url anchor.download = 'live-component-recovery-codes.txt' anchor.click() URL.revokeObjectURL(url) } return (

{t('auth.recovery_description')}

{codes.length > 0 ? (
{codes.map(code => ( {code} ))}
) : (
{t('auth.no_recovery_codes')}
)}
{codes.length > 0 && ( <> )}
) } export function LoginPage({ onAuthenticated, setupRequired, }: { onAuthenticated: () => Promise setupRequired: boolean }) { const { t } = useI18n() const [username, setUsername] = useState('') const [totpCode, setTotpCode] = useState('') const [useRecoveryCode, setUseRecoveryCode] = useState(false) const [error, setError] = useState('') const [busy, setBusy] = useState(false) usePwaUpdateBlocker('login-form', t('auth.login_blocker'), busy || Boolean(username || totpCode)) const submit = async (event: FormEvent) => { event.preventDefault() setBusy(true) setError('') try { await api('/api/v1/auth/login', json('POST', { username: username.trim(), code: totpCode })) setTotpCode('') await onAuthenticated() location.assign('/control/') } catch (reason) { setError(errorMessage(reason, t('auth.login_failed'))) } finally { setBusy(false) } } return ( {setupRequired ? ( <> {t('auth.first_deployment')} {t('auth.create_admin')} ) : ( <> {t('auth.have_invitation')}{' '} {t('auth.register_account')} )}

} >

{t('auth.passwordless_description')}

{error && (
{error}
)}
) } export function EnrollmentPage({ mode, onAuthenticated, }: { mode: 'setup' | 'register' onAuthenticated: () => Promise }) { const { language, t } = useI18n() const inviteFromFragment = new URLSearchParams(location.hash.slice(1)).get('invite') ?? '' const [inviteCode, setInviteCode] = useState(inviteFromFragment) const [username, setUsername] = useState('') const [bootstrapPassword, setBootstrapPassword] = useState('') const [enrollment, setEnrollment] = useState() const [totpCode, setTotpCode] = useState('') const [recoveryCodes, setRecoveryCodes] = useState() const [error, setError] = useState('') const [busy, setBusy] = useState(false) const isSetup = mode === 'setup' usePwaUpdateBlocker( 'totp-enrollment', enrollment || recoveryCodes ? t('auth.enrollment_blocker') : t('auth.registration_blocker'), busy || Boolean( inviteCode || username || bootstrapPassword || totpCode || enrollment || recoveryCodes, ), ) const start = async (event: FormEvent) => { event.preventDefault() setBusy(true) setError('') try { const payload = await api( `/api/v1/auth/${isSetup ? 'setup' : 'register'}/start`, json('POST', { ...(isSetup ? {} : { inviteCode: inviteCode.trim() }), username: username.trim(), ...(isSetup ? { bootstrapPassword } : {}), }), ) const next = normalizeEnrollment(payload) if (!next.enrollmentToken) throw new Error(t('auth.registration_missing_id')) setEnrollment(next) setBootstrapPassword('') if (!isSetup) { setInviteCode('') history.replaceState(null, '', authRoute('register')) } } catch (reason) { setError(errorMessage(reason, t('auth.registration_start_failed'))) } finally { setBusy(false) } } const confirm = async (event: FormEvent) => { event.preventDefault() if (!enrollment) return setBusy(true) setError('') try { const payload = await api( `/api/v1/auth/${isSetup ? 'setup' : 'register'}/confirm`, json('POST', { enrollmentToken: enrollment.enrollmentToken, code: totpCode, language, }), ) setTotpCode('') // Drop QR/manual-key material from React state as soon as enrollment is // committed; only the one-time recovery codes remain on screen. setEnrollment(undefined) setRecoveryCodes(normalizeRecoveryCodes(payload)) } catch (reason) { setError(errorMessage(reason, t('auth.registration_confirm_failed'))) } finally { setBusy(false) } } const finish = async () => { await onAuthenticated() location.assign('/control/') } if (recoveryCodes) return void finish()} /> if (enrollment) { return (
{error && (
{error}
)}
) } return ( {t('auth.already_account')} {t('auth.back_to_login')}

} >

{isSetup ? t('auth.setup_description') : t('auth.register_description')}

{!isSetup && ( )} {isSetup && ( )} {error && (
{error}
)}
) }