import { PasswordChangeRequest } 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 } from "~/api/auth.ts"; import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx"; import { SecretInput } from "~/components/console/secret-input.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 { PasskeyBlock } from "./account-passkeys.tsx"; import { TotpBlock } from "./account-totp.tsx"; import { ApiKeysSection } from "./api-keys.tsx"; import { SettingsBlock } from "./settings-block.tsx"; // 账户与安全:改密、二次验证(TOTP,account-totp.tsx)、Passkey(account-passkeys.tsx)、当前会话与本机 Token、 // 签发给外部程序的 API Key(api-keys.tsx)。快捷键在「界面」。 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 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 ? "已设置。" : "未设置。"}

{hasToken ? ( ) : null}
); } export function AccountSection() { const session = useSession(); const cookie = !!session.data?.expiresAt; return (
); }