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 type { TotpEnrollment } from './types' function AuthShell({ eyebrow, title, children, footer }: { eyebrow: string title: string children: ReactNode footer?: ReactNode }) { return (

{eyebrow}

{title}

{children} {footer && }
) } function TotpQr({ enrollment }: { enrollment: TotpEnrollment }) { 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 ? TOTP 验证器绑定二维码 : 二维码暂不可用,请使用右侧密钥手工添加。}

绑定动态验证器

  1. 使用 1Password、Aegis、Microsoft Authenticator 等应用扫描二维码。
  2. 若无法扫码,选择“输入设置密钥”。
  3. 输入应用中出现的 6 位动态码完成绑定。
) } function RecoveryCodes({ codes, onContinue }: { codes: string[]; onContinue: () => void }) { const [copied, setCopied] = useState(false) const text = codes.join('\n') const download = () => { const blob = new Blob([ '洛星瓷直播组件恢复码\n', '每个恢复码只能使用一次,请离线妥善保存。\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 = 'luoxingci-recovery-codes.txt' anchor.click() URL.revokeObjectURL(url) } return (

手机丢失或验证器不可用时,可用恢复码登录。服务端不会再次显示这些明文恢复码。

{codes.length > 0 ?
{codes.map(code => {code})}
:
服务端没有返回恢复码,请先联系管理员确认恢复策略。
}
{codes.length > 0 && ( <> )}
) } export function LoginPage({ onAuthenticated, setupRequired }: { onAuthenticated: () => Promise setupRequired: boolean }) { 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', '完成或清空正在填写的登录表单', 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, '用户名或动态验证码不正确')) } finally { setBusy(false) } } return ( {setupRequired ? <>首次部署?创建系统管理员 : <>持有邀请码?注册新账户}

)} >

这是无密码账户。输入用户名与验证器中的动态验证码即可登录。

{error &&
{error}
}
) } export function EnrollmentPage({ mode, onAuthenticated }: { mode: 'setup' | 'register' onAuthenticated: () => Promise }) { 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 ? '完成 TOTP 绑定并保存一次性恢复码' : '完成或清空正在填写的注册表单', 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('服务端未返回注册流程标识') setEnrollment(next) setBootstrapPassword('') if (!isSetup) { setInviteCode('') history.replaceState(null, '', authRoute('register')) } } catch (reason) { setError(errorMessage(reason, '无法开始安全注册流程')) } 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 }), ) 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, '动态验证码无效或注册流程已过期')) } finally { setBusy(false) } } const finish = async () => { await onAuthenticated() location.assign('/control/') } if (recoveryCodes) return void finish()} /> if (enrollment) { return (
{error &&
{error}
}
) } return ( 已有账户?返回登录

} >

{isSetup ? '首位账户将拥有邀请码管理权限。旧管理员口令只授权这一次初始化,不会成为账户密码。' : '邀请码只用于注册;这是无密码账户,创建后每次登录都必须验证 TOTP。'}

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