import type { SiteAccount, SiteCredentialsBody } from "@umt/contracts"; import { FormField } from "~/components/console/form-field.tsx"; import { SecretInput } from "~/components/console/secret-input.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { secretHint, secretPatch } from "~/lib/secret.ts"; import { cn } from "~/lib/utils.ts"; import { AUTH_KIND_LABEL, type LoginKind } from "./site-fields.ts"; // 凭证三选一(账号密码 / Cookie / API Key)。只写不回显;添加站点向导与"更新凭证"对话框共用。 // 更新已有账号且类型不变时,已存的密码 / Cookie / API Key / TOTP 显示掩码占位,留空 = 保持原值(值约定见 ~/lib/secret.ts)。 /** 已存账号里与表单相关的非密信息 */ export type SavedCredential = { kind: LoginKind; hasTotp: boolean }; /** 已有账号 → 表单初值:同类型时带上登录名与 UA(非密),凭证留空表示保持 */ export function draftFromAccount(kind: LoginKind, acc: SiteAccount | null | undefined): CredentialDraft { const d = emptyCredential(kind); if (!acc || acc.authKind !== kind) return d; return { ...d, username: acc.loginName ?? "", userAgent: acc.userAgent ?? "" }; } export type CredentialDraft = { kind: LoginKind; username: string; password: string; totpSecret: string; saveCookieOnly: boolean; cookie: string; apiKey: string; userAgent: string; }; export const emptyCredential = (kind: LoginKind): CredentialDraft => ({ kind, username: "", password: "", totpSecret: "", saveCookieOnly: false, cookie: "", apiKey: "", userAgent: "", }); /** 草稿 → 请求体;缺必填返回字段错误 */ export function credentialBody( d: CredentialDraft, label: string, saved?: SavedCredential | null, ): { body: SiteCredentialsBody; errors: null } | { body: null; errors: Record } { const ua = d.userAgent.trim() || undefined; // 同类型已存:密码 / Cookie / API Key 可以留空保持 const keep = saved?.kind === d.kind; if (d.kind === "password") { const errors: Record = {}; if (!d.username.trim()) errors.username = "填写登录名"; const password = secretPatch(d.password); if (!password && !keep) errors.password = "填写密码"; if (Object.keys(errors).length) return { body: null, errors }; const totp = secretPatch(d.totpSecret); return { body: { kind: "password", label, username: d.username.trim(), ...(password ? { password } : {}), ...(totp !== undefined ? { totpSecret: totp } : {}), saveCookieOnly: d.saveCookieOnly || undefined, userAgent: ua, }, errors: null, }; } if (d.kind === "cookie") { const cookie = secretPatch(d.cookie); if (!cookie && !keep) return { body: null, errors: { cookie: "粘贴 Cookie" } }; return { body: { kind: "cookie", label, ...(cookie ? { cookie } : {}), userAgent: ua }, errors: null }; } const apiKey = secretPatch(d.apiKey); if (!apiKey && !keep) return { body: null, errors: { apiKey: "填写 API Key" } }; return { body: { kind: "api_key", label, ...(apiKey ? { apiKey } : {}) }, errors: null }; } export function CredentialFields({ value, onChange, kinds, errors, disabled, idPrefix = "cred", saved, }: { value: CredentialDraft; onChange: (v: CredentialDraft) => void; kinds: LoginKind[]; errors: Record; disabled?: boolean; idPrefix?: string; /** 更新已有账号时传:同类型的已存凭证显示掩码,留空保持 */ saved?: SavedCredential | null; }) { const set = (k: K, v: CredentialDraft[K]) => onChange({ ...value, [k]: v }); const isSet = saved?.kind === value.kind; const keepHint = (h?: string) => (isSet ? (h ? `${h};留空不修改` : "留空不修改") : h); const id = (s: string) => `${idPrefix}-${s}`; return (
{kinds.length > 1 ? (

凭证类型

{kinds.map((k) => ( // biome-ignore lint/a11y/useSemanticElements: 分段按钮,radio 语义由 role 表达 ))}
) : null} {value.kind === "password" ? ( <>
set("username", e.target.value)} disabled={disabled} /> set("password", v)} disabled={disabled} />
set("totpSecret", v)} disabled={disabled} />
set("saveCookieOnly", v)} disabled={disabled} />
) : value.kind === "cookie" ? ( set("cookie", v)} disabled={disabled} /> ) : ( set("apiKey", v)} disabled={disabled} /> )} {value.kind !== "api_key" ? ( set("userAgent", e.target.value)} disabled={disabled} /> ) : null}
); }