diff --git a/apps/csms/src/lib/auth.ts b/apps/csms/src/lib/auth.ts index 342fffc..8605bb4 100644 --- a/apps/csms/src/lib/auth.ts +++ b/apps/csms/src/lib/auth.ts @@ -13,6 +13,7 @@ export const auth = betterAuth({ }, }), trustedOrigins: [process.env.WEB_ORIGIN ?? "http://localhost:3000"], + appName: "Helios EVCS", user: { additionalFields: {}, }, diff --git a/apps/web/app/dashboard/settings/page.tsx b/apps/web/app/dashboard/settings/page.tsx index 847dd47..f6319bd 100644 --- a/apps/web/app/dashboard/settings/page.tsx +++ b/apps/web/app/dashboard/settings/page.tsx @@ -1,8 +1,8 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; -import { Alert, Button, CloseButton, Spinner } from "@heroui/react"; -import { Fingerprint, TrashBin } from "@gravity-ui/icons"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Alert, Button, CloseButton, Input, Label, Spinner, TextField } from "@heroui/react"; +import { Fingerprint, Pencil, TrashBin, Xmark, Check } from "@gravity-ui/icons"; import { authClient } from "@/lib/auth-client"; type Passkey = { @@ -15,10 +15,15 @@ type Passkey = { export default function SettingsPage() { const [passkeys, setPasskeys] = useState([]); const [loading, setLoading] = useState(true); - const [adding, setAdding] = useState(false); + const [addingName, setAddingName] = useState(null); // null = collapsed + const [registering, setRegistering] = useState(false); const [deletingId, setDeletingId] = useState(null); + const [renamingId, setRenamingId] = useState(null); + const [renameValue, setRenameValue] = useState(""); + const [renameSaving, setRenameSaving] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); + const renameInputRef = useRef(null); const loadPasskeys = useCallback(async () => { setLoading(true); @@ -36,22 +41,33 @@ export default function SettingsPage() { void loadPasskeys(); }, [loadPasskeys]); - const handleAdd = async () => { + const handleStartAdd = () => { + setAddingName(""); setError(""); setSuccess(""); - setAdding(true); + }; + + const handleCancelAdd = () => setAddingName(null); + + const handleRegister = async () => { + setError(""); + setSuccess(""); + setRegistering(true); try { - const res = await authClient.passkey.addPasskey(); + const res = await authClient.passkey.addPasskey({ + name: addingName?.trim() || undefined, + }); if (res?.error) { setError(res.error.message ?? "注册 Passkey 失败"); } else { setSuccess("Passkey 注册成功"); + setAddingName(null); await loadPasskeys(); } } catch { setError("注册 Passkey 失败,请重试"); } finally { - setAdding(false); + setRegistering(false); } }; @@ -74,6 +90,40 @@ export default function SettingsPage() { } }; + const startRename = (pk: Passkey) => { + setRenamingId(pk.id); + setRenameValue(pk.name ?? ""); + setTimeout(() => renameInputRef.current?.focus(), 50); + }; + + const cancelRename = () => { + setRenamingId(null); + setRenameValue(""); + }; + + const handleRename = async (id: string) => { + setError(""); + setRenameSaving(true); + try { + const res = await (authClient.passkey as any).updatePasskey({ + id, + name: renameValue.trim() || undefined, + }); + if (res?.error) { + setError(res.error.message ?? "重命名失败"); + } else { + setPasskeys((prev) => + prev.map((p) => (p.id === id ? { ...p, name: renameValue.trim() || null } : p)), + ); + setRenamingId(null); + } + } catch { + setError("重命名失败,请重试"); + } finally { + setRenameSaving(false); + } + }; + return (
@@ -112,11 +162,38 @@ export default function SettingsPage() {

使用设备生物识别或 PIN 免密登录

- + {addingName === null && ( + + )} + {/* Inline add form */} + {addingName !== null && ( +
+ + + setAddingName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handleRegister(); + if (e.key === "Escape") handleCancelAdd(); + }} + /> + + + +
+ )} + {loading ? (
@@ -126,13 +203,58 @@ export default function SettingsPage() { ) : (
    {passkeys.map((pk) => ( -
  • -
    +
  • +
    -
    -

    - {pk.name ?? pk.deviceType ?? "Passkey"} -

    +
    + {/* Name row: text+pencil or inline input */} +
    + {renamingId === pk.id ? ( + <> + setRenameValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handleRename(pk.id); + if (e.key === "Escape") cancelRename(); + }} + placeholder="输入名称" + /> + + + + ) : ( + <> +

    + {pk.name ?? pk.deviceType ?? "Passkey"} +

    + + + )} +
    + {/* Date row: always visible */}

    添加于{" "} {new Date(pk.createdAt).toLocaleString("zh-CN", { @@ -147,7 +269,7 @@ export default function SettingsPage() { isIconOnly size="sm" variant="danger-soft" - isDisabled={deletingId === pk.id} + isDisabled={deletingId === pk.id || renamingId === pk.id} onPress={() => handleDelete(pk.id)} > {deletingId === pk.id ? : }