import type { PasskeyInfo } from "@umt/contracts"; import { Check, KeyRound, LoaderCircle, Pencil, Plus, Trash2, X } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { isPasskeyCancel, passkeySupported, useDeletePasskey, usePasskeys, useRegisterPasskey, useRenamePasskey, } from "~/api/auth.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { useListPagination } from "~/components/console/use-list-pagination.ts"; import { Button } from "~/components/ui/button.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Pagination } from "~/components/ui/pagination.tsx"; import { errorMessage } from "~/lib/api.ts"; import { formatDateTime } from "~/lib/utils.ts"; import { SettingsBlock } from "./settings-block.tsx"; // Passkey:登记(可以多把,各自命名)、改名、删除。登录页有 Passkey 时显示"用 Passkey 登录"。 /** 按浏览器平台给个默认名,方便区分 */ function defaultName(): string { const ua = typeof navigator === "undefined" ? "" : navigator.userAgent; if (/iPhone|iPad/.test(ua)) return "iPhone / iPad"; if (/Mac OS X/.test(ua)) return "Mac"; if (/Android/.test(ua)) return "Android"; if (/Windows/.test(ua)) return "Windows"; return "Passkey"; } function PasskeyRow({ item }: { item: PasskeyInfo }) { const rename = useRenamePasskey(); const del = useDeletePasskey(); const [editing, setEditing] = useState(false); const [name, setName] = useState(item.name); const save = async (e: React.FormEvent) => { e.preventDefault(); const v = name.trim(); if (!v || v === item.name) return setEditing(false); try { await rename.mutateAsync({ id: item.id, name: v }); setEditing(false); } catch (err) { toast.error(errorMessage(err)); } }; return (
  • {editing ? (
    setName(e.target.value)} className="h-8 max-w-64" />
    ) : (

    {item.name}

    {formatDateTime(item.createdAt)} 登记 ·{" "} {item.lastUsedAt ? `${formatDateTime(item.lastUsedAt)} 用过` : "还没用过"} {item.backedUp ? " · 已同步到云端钥匙串" : ""}

    )} {editing ? null : (
    } title={`删除 Passkey「${item.name}」?`} description="删除后这把钥匙不能再登录控制台;设备上的钥匙要在系统设置里另外删。" confirmLabel="删除 Passkey" destructive onConfirm={async () => { try { await del.mutateAsync(item.id); toast.success("Passkey 已删除"); } catch (err) { toast.error(errorMessage(err)); throw err; } }} />
    )}
  • ); } export function PasskeyBlock({ disabled }: { disabled: boolean }) { const list = usePasskeys(!disabled); const register = useRegisterPasskey(); const [adding, setAdding] = useState(false); const [name, setName] = useState(""); const supported = passkeySupported(); const items = list.data?.items ?? []; const paged = useListPagination(items, { resetKey: String(disabled) }); const add = async (e: React.FormEvent) => { e.preventDefault(); try { const r = await register.mutateAsync(name.trim() || defaultName()); toast.success(`已登记「${r.name}」`); setAdding(false); setName(""); } catch (err) { if (isPasskeyCancel(err)) return; toast.error(errorMessage(err, "登记失败")); } }; let body: React.ReactNode; if (disabled) body =

    当前用 API Key 访问,登录会话才能修改。

    ; else body = (
    {list.isPending ? (

    读取中…

    ) : items.length ? ( ) : (

    还没有登记 Passkey。

    )} {items.length > 0 ? : null} {!supported ? (

    这个浏览器用不了 Passkey:需要 HTTPS 或 localhost 访问。

    ) : adding ? (
    setName(e.target.value)} className="min-w-0 flex-1" />
    ) : (
    )}
    ); return ( {body} ); }