feat!: sensitive actions need a fresh passkey touch (elevation windows), plus login page hint cleanup [CI]
This commit is contained in:
1 parent
1379b8c7cb
commit
ff45ad086e
27 files changed
+1305
-58
No files matched your search
+2
-13
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user