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
?

:
二维码暂不可用,请使用右侧密钥手工添加。}
绑定动态验证器
- 使用 1Password、Aegis、Microsoft Authenticator 等应用扫描二维码。
- 若无法扫码,选择“输入设置密钥”。
- 输入应用中出现的 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
? <>首次部署?创建系统管理员>
: <>持有邀请码?注册新账户>}
)}
>
这是无密码账户。输入用户名与验证器中的动态验证码即可登录。
)
}
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 (
)
}
return (
已有账户?返回登录}
>
{isSetup
? '首位账户将拥有邀请码管理权限。旧管理员口令只授权这一次初始化,不会成为账户密码。'
: '邀请码只用于注册;这是无密码账户,创建后每次登录都必须验证 TOTP。'}
)
}