feat!: sensitive actions need a fresh passkey touch (elevation windows), plus login page hint cleanup [CI]

This commit is contained in:
justme committed 2026-10-07 22:29:24 +08:00
1 parent 1379b8c7cb
commit ff45ad086e
27 files changed
+1305 -58

No files matched your search

+2 -13
View File
@@ -7,6 +7,7 @@ import { conditionalSupported, passkeySupported } from '@shared/api/webauthn'
import { describeError, Field, Segmented, SourceButton, Spinner, ThemeToggle } from '@shared/ui'
import { useAuth } from './auth'
import { BackendUpdateNotice } from './BackendUpdateNotice'
import { ElevationBar } from './ElevationBar'
import { PasskeyNotice } from './PasskeyNotice'
import { AgentDetail } from './pages/AgentDetail'
import { Agents } from './pages/Agents'
@@ -81,8 +82,6 @@ function Login() {
}
}
const conditional = useRef<AbortController | null>(null) // 正在挂着的自动填充请求:点按钮之前要先取消,浏览器同一时间只允许一个 WebAuthn 请求
// 自动填充是否可用:unknown 还在检测,yes 已经挂上(点用户名框会列出已保存的 passkey),no 浏览器不支持
const [autofill, setAutofill] = useState<'unknown' | 'yes' | 'no'>('unknown')
const retryOnFocus = useRef(false) // 服务器拒绝了自动填充选的 passkey:等用户再点输入框时才重新开始,不自己循环
const usePasskey = async () => {
setBusy(true)
@@ -110,7 +109,6 @@ function Login() {
let expired = false // 只有「挑战到期」才重新开始;组件卸载、依赖变化时的取消不能再触发重来(否则会形成循环)
void conditionalSupported().then(async (ok) => {
if (abort.signal.aborted) return
setAutofill(ok ? 'yes' : 'no')
if (!ok) return
timer = setTimeout(() => {
expired = true
@@ -194,7 +192,6 @@ function Login() {
) : null}
{usingPasskey ? (
<>
<p className="muted">用指纹、人脸或安全密钥登录,不用输用户名和密码。点下面的按钮,浏览器会让你选一个这个站点的 Passkey。</p>
{error ? <div className="notice bad">{error}</div> : null}
<button className="btn primary" type="button" disabled={busy} onClick={() => void usePasskey()} autoFocus>
<KeyRound size={16} />
@@ -214,15 +211,6 @@ function Login() {
<Field label="用户名">
<input className="input" name="username" value={username} onChange={(e) => setUsername(e.target.value)} autoComplete={autofillActive ? 'username webauthn' : 'username'} autoFocus={!adding} required onFocus={retry} onClick={retry} />
</Field>
{canPasskey ? (
<p className="faint" style={{ margin: '-6px 0 0', fontSize: 12 }}>
{autofill === 'yes'
? '点一下用户名框:如果浏览器里已经保存了这个站点的 Passkey,会列在下拉里,选一个就直接登录。还没有的话,先用密码登录,到「系统 → 用户」添加。'
: autofill === 'no'
? '这个浏览器不支持从输入框里选 Passkey,请切到上面的「Passkey」。'
: ''}
</p>
) : null}
<Field label="密码">
<input className="input" name="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" required />
</Field>
@@ -273,6 +261,7 @@ export function App() {
</div>
))}
<div className="spacer" />
<ElevationBar />
<SourceButton variant="status" />
<div className="me">
<span className="avatar">{(me.username ?? 'A').slice(0, 1).toUpperCase()}</span>