import { PasswordChangeRequest, TotpCode } from "@umt/contracts"; import { LoaderCircle, LogOut } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { z } from "zod"; import { useChangePassword, useLogout, useSession, useTotpAction, useTotpStatus } from "~/api/auth.ts"; import { AgentShortcutsHelp } from "~/components/console/agent-launcher.tsx"; import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Input } from "~/components/ui/input.tsx"; import { clearAuth, errorMessage, getToken, LOGIN_PATH, setToken } from "~/lib/api.ts"; import { formatDateTime } from "~/lib/utils.ts"; import { anchorId, SettingsBlock } from "./settings-block.tsx"; // 账户与安全:改密、二次验证(TOTP)、当前会话与本机 Token、快捷键 const PasswordForm = PasswordChangeRequest.extend({ confirm: z.string() }).refine((v) => v.newPassword === v.confirm, { message: "两次输入的新密码不一致", path: ["confirm"], }); const EMPTY_PWD = { currentPassword: "", newPassword: "", confirm: "" }; function PasswordBlock({ disabled }: { disabled: boolean }) { const change = useChangePassword(); const [form, setForm] = useState(EMPTY_PWD); const [errors, setErrors] = useState>({}); const dirty = Object.values(form).some(Boolean); const submit = async (e: React.FormEvent) => { e.preventDefault(); const parsed = PasswordForm.safeParse(form); if (!parsed.success) { setErrors(zodFieldErrors(parsed.error.issues)); return; } setErrors({}); try { const r = await change.mutateAsync({ currentPassword: form.currentPassword, newPassword: form.newPassword }); toast.success(r.revokedSessions ? `密码已修改,注销了其它 ${r.revokedSessions} 个会话` : "密码已修改"); setForm(EMPTY_PWD); } catch (err) { const msg = errorMessage(err); if (/旧密码|当前密码|password/i.test(msg)) setErrors({ currentPassword: "当前密码不正确" }); else toast.error(msg); } }; const set = (k: keyof typeof form) => (e: React.ChangeEvent) => setForm((f) => ({ ...f, [k]: e.target.value })); const off = disabled || change.isPending; return (
); } function TotpBlock({ disabled }: { disabled: boolean }) { const status = useTotpStatus(!disabled); const action = useTotpAction(); const [setup, setSetup] = useState<{ secret: string; uri: string } | null>(null); const [code, setCode] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const enabled = status.data?.enabled === true; const reset = () => { setSetup(null); setCode(""); setPassword(""); setError(null); }; const begin = async () => { setError(null); try { const r = await action.mutateAsync({ action: "begin" }); if (r.secret && r.otpauthUri) setSetup({ secret: r.secret, uri: r.otpauthUri }); } catch (err) { setError(errorMessage(err)); } }; const submit = async (e: React.FormEvent) => { e.preventDefault(); if (!TotpCode.safeParse(code).success) { setError("验证码为 6 位数字"); return; } if (enabled && !password) { setError("输入当前密码"); return; } setError(null); try { if (enabled) { await action.mutateAsync({ action: "disable", password, code }); toast.success("二次验证已关闭"); } else { await action.mutateAsync({ action: "enable", code }); toast.success("二次验证已开启"); } reset(); } catch (err) { setError(errorMessage(err)); } }; const grouped = setup?.secret.replace(/(.{4})/g, "$1 ").trim(); return ( {enabled ? "已开启" : "未开启"} ) } > {disabled ? (

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

) : !enabled && !setup ? (
{status.data?.pending ? (

有一个未确认的密钥,重新生成会作废它。

) : null} {error ?

{error}

: null}
) : (
{setup ? (

在验证器里手动输入密钥,或粘贴 otpauth 链接。密钥只显示这一次。

{grouped} {setup.uri}
) : null} {enabled ? ( setPassword(e.target.value)} /> ) : null} setCode(e.target.value.replace(/\D/g, ""))} aria-invalid={!!error} />
{setup ? ( ) : null}
)}
); } function SessionBlock() { const session = useSession(); const logout = useLogout(); const [hasToken, setHasToken] = useState(() => !!getToken()); const [token, setTokenValue] = useState(""); const s = session.data; const doLogout = async () => { try { await logout.mutateAsync(); } catch { // 会话已失效也照样回登录页 } window.location.assign(LOGIN_PATH); }; return (
身份
{s?.actor?.name ?? "—"} · {s?.actor?.type === "api_key" ? "API Key" : "管理员"}
当前会话
{s?.expiresAt ? `到 ${formatDateTime(s.expiresAt)} 失效` : "未使用登录会话"}
其它会话
修改密码时全部注销

本机 Token

只存在这个浏览器,有值时请求会带上它。{hasToken ? "已设置。" : "未设置。"}

setTokenValue(e.target.value)} className="min-w-0 flex-1" /> {hasToken ? ( ) : null}
); } export function AccountSection() { const session = useSession(); const cookie = !!session.data?.expiresAt; return (
{/* 快捷键说明自带标题,只包卡片与锚点 */}
); }