Files
MoShiro/web/admin/src/pages/PasskeyAdd.tsx
T

58 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Plus } from 'lucide-react'
import { useState } from 'react'
import { passkeySupported, registerPasskey, type PasskeyPlace } from '@shared/api/webauthn'
import { guessPlatform, useAction, useToast } from '@shared/ui'
/** 默认的 passkey 名称:按浏览器所在的系统给一个能认出来的名字。 */
function defaultName(): string {
const platform = guessPlatform()
return platform === 'windows' ? 'Windows 设备' : platform === 'macos' ? 'Mac' : 'Linux 设备'
}
/**
* 添加 passkey 的表单:名称 + 保存位置 + 按钮。「我的 Passkey」和登录后的提醒共用同一份。
* 保存位置只是给浏览器的提示:`device` 优先用设备或浏览器账户自带的(Chrome 里是 Google 密码管理工具、Windows Hello),
* `external` 是安全密钥或手机扫码。
*/
export function PasskeyAddForm({ onAdded, compact }: { onAdded: () => void; compact?: boolean }) {
const { busy, run } = useAction()
const toast = useToast()
const [name, setName] = useState(defaultName)
const [place, setPlace] = useState<PasskeyPlace>('device')
const supported = passkeySupported()
return (
<div className={compact ? 'stack' : 'row wrap'} style={{ marginTop: compact ? 0 : 12 }}>
<input className="input" style={{ maxWidth: compact ? undefined : 220 }} placeholder="名称,例如 MacBook" value={name} onChange={(e) => setName(e.target.value)} disabled={!supported} />
<select className="input" style={{ maxWidth: compact ? undefined : 320 }} value={place} onChange={(e) => setPlace(e.target.value as PasskeyPlace)} disabled={!supported} title="保存位置只是给浏览器的提示,最终由浏览器和系统决定">
<option value="device">保存在此设备或浏览器账户(Google 密码管理工具、Windows Hello)</option>
<option value="external">安全密钥,或用手机扫码</option>
</select>
<button
type="button"
className="btn primary"
disabled={busy || !supported || !name.trim()}
title={supported ? '' : '这个浏览器不支持 Passkey(需要 HTTPS 或 localhost)'}
onClick={async () => {
let added: string | undefined
const ok = await run(async () => {
try {
added = (await registerPasskey(name.trim(), place)).name
} catch (e) {
if (e instanceof DOMException && e.name === 'NotAllowedError') throw new Error('已取消')
throw e
}
})
if (ok) {
// 成功提示停 10 秒:说清楚存了什么、下一步怎么用,点一下可以提前关掉
toast(`「${added}」已经保存。下次登录时点用户名框选它,或者点「使用 Passkey 登录」;建议再添加一个备用的。`, false, { title: 'Passkey 添加成功', ms: 10_000 })
onAdded()
}
}}
>
<Plus size={18} />
添加 Passkey
</button>
</div>
)
}