import { App, Button, Form, Input, Select, Skeleton, Switch } from "antd"; import { AlertTriangle, BadgeCheck, ChevronDown, Globe2, KeyRound, LockKeyhole, RefreshCw, RotateCcw, Save, ShieldCheck, UserPlus, UsersRound } from "lucide-react"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useBlocker } from "react-router"; import { cn } from "@/lib/utils"; import { getAdminLinuxDOSetting, getAdminRegistrationSetting, updateAdminLinuxDOSetting, updateAdminRegistrationSetting, type LinuxDOSetting, type RegistrationSetting } from "@/services/api/wallet"; import { AdminStatusBadge, configuredSecretText, SettingsSectionCard } from "./admin-ui"; type LinuxDOFormValues = Omit; export default function AccessSettingsPanel() { const { message, modal } = App.useApp(); const [linuxdo, setLinuxdo] = useState(null); const [registration, setRegistration] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [savingLinuxDO, setSavingLinuxDO] = useState(false); const [savingRegistration, setSavingRegistration] = useState(false); const [dirty, setDirty] = useState(false); const [draftLinuxDOEnabled, setDraftLinuxDOEnabled] = useState(false); const [loadError, setLoadError] = useState(""); const [saveError, setSaveError] = useState(""); const [form] = Form.useForm(); const requestVersionRef = useRef(0); const navigationConfirmOpenRef = useRef(false); const navigationTriggerRef = useRef(null); const load = useCallback( async (initial = false, announce = false) => { const requestVersion = ++requestVersionRef.current; if (initial) setLoading(true); else setRefreshing(true); setLoadError(""); try { const [linuxdoData, registrationData] = await Promise.all([getAdminLinuxDOSetting(), getAdminRegistrationSetting()]); if (requestVersion !== requestVersionRef.current) return; setLinuxdo(linuxdoData.setting); setRegistration(registrationData.setting); setDirty(false); setSaveError(""); if (announce) message.success("已重新读取当前登录与注册配置"); } catch (error) { if (requestVersion !== requestVersionRef.current) return; const errorMessage = error instanceof Error ? error.message : "读取登录与注册配置失败"; setLoadError(errorMessage); if (!initial) message.error(errorMessage); } finally { if (requestVersion === requestVersionRef.current) { setLoading(false); setRefreshing(false); } } }, [form, message], ); useEffect(() => { void load(true); return () => { requestVersionRef.current += 1; }; }, [load]); useEffect(() => { if (loading || !linuxdo || !registration) return; form.setFieldsValue(toLinuxDOFormValues(linuxdo)); setDraftLinuxDOEnabled(linuxdo.enabled); }, [form, linuxdo, loading, registration]); const blocker = useBlocker(dirty && !savingLinuxDO); useEffect(() => { const beforeUnload = (event: BeforeUnloadEvent) => { if (!dirty || savingLinuxDO) return; event.preventDefault(); }; window.addEventListener("beforeunload", beforeUnload); return () => window.removeEventListener("beforeunload", beforeUnload); }, [dirty, savingLinuxDO]); useEffect(() => { if (blocker.state !== "blocked" || navigationConfirmOpenRef.current) return; navigationConfirmOpenRef.current = true; navigationTriggerRef.current = document.activeElement instanceof HTMLElement && document.activeElement !== document.body ? document.activeElement : null; modal.confirm({ title: "放弃 Linux.do 登录调整?", content: "当前表单有尚未保存的调整,离开后这些内容会丢失。用户注册状态不受影响。", okText: "放弃并离开", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => { navigationConfirmOpenRef.current = false; navigationTriggerRef.current = null; blocker.proceed(); }, onCancel: () => { navigationConfirmOpenRef.current = false; blocker.reset(); window.requestAnimationFrame(() => { const fallback = document.querySelector(".admin-access-command-actions button"); const target = navigationTriggerRef.current?.isConnected ? navigationTriggerRef.current : fallback; target?.focus(); navigationTriggerRef.current = null; }); }, }); }, [blocker, modal]); const resetLinuxDODraft = () => { if (!linuxdo || savingLinuxDO) return; form.setFieldsValue(toLinuxDOFormValues(linuxdo)); form.setFields([]); setDraftLinuxDOEnabled(linuxdo.enabled); setDirty(false); setSaveError(""); message.info("已撤销 Linux.do 登录的未保存调整"); }; const requestRefresh = () => { if (!dirty) { void load(false, true); return; } modal.confirm({ title: "放弃调整并重新读取?", content: "重新读取会丢弃当前 Linux.do 表单中的未保存内容,并以服务端配置为准。", okText: "放弃并刷新", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => load(false, true), }); }; const toggleRegistration = async (enabled: boolean) => { setSavingRegistration(true); try { const data = await updateAdminRegistrationSetting(enabled); setRegistration(data.setting); message.success(enabled ? "用户注册已开启" : "用户注册已关闭"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "更新注册设置失败"; message.error(errorMessage); throw error; } finally { setSavingRegistration(false); } }; const requestRegistrationChange = (enabled: boolean) => { if (!registration || enabled === registration.enabled || savingRegistration) return; void toggleRegistration(enabled).catch(() => undefined); }; const saveLinuxDO = async (values: LinuxDOFormValues) => { const expected = normalizeLinuxDOFormValues(values); setSavingLinuxDO(true); setSaveError(""); try { const result = await updateAdminLinuxDOSetting(expected); if (!linuxDOResponseMatches(result.setting, expected)) throw new Error("服务端返回的 Linux.do 配置与本次保存内容不一致,请重新读取后核对"); setLinuxdo(result.setting); setDirty(false); message.success("Linux.do 登录配置已保存"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "保存 Linux.do 配置失败"; setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`); message.error(errorMessage); throw error; } finally { setSavingLinuxDO(false); } }; const submitLinuxDOSave = async () => { let values: LinuxDOFormValues; try { values = await form.validateFields(); } catch { return; } const validationError = validateLinuxDODraft(values, linuxdo); if (validationError) { message.error(validationError); return; } try { await saveLinuxDO(values); } catch { // 保存错误已在 saveLinuxDO 中就地提示。 } }; const toggleLinuxDO = (enabled: boolean) => { if (!linuxdo || savingLinuxDO) return; form.setFieldValue("enabled", enabled); setDraftLinuxDOEnabled(enabled); setDirty(hasLinuxDOChanges({ ...form.getFieldsValue(true), enabled }, linuxdo)); setSaveError(""); }; if (loading && (!linuxdo || !registration)) { return (
); } if (!linuxdo || !registration) { return (

无法读取登录与注册配置

{loadError || "当前没有可显示的配置,请稍后重试。"}

); } return (
{dirty ? "Linux.do 有未保存的调整" : "登录与注册设置"}

{dirty ? "完成接入信息后保存生效。" : `新用户注册${registration.enabled ? "已开放" : "已关闭"} · Linux.do ${linuxdo.enabled ? "已启用" : "未启用"}`}

{dirty ? ( ) : null}
{loadError || saveError ? (
) : null}
); } function FormSectionTitle({ icon, title, description }: { icon: ReactNode; title: string; description: string }) { return (
{icon}

{title}

{description}

); } function toLinuxDOFormValues(setting: LinuxDOSetting): LinuxDOFormValues { return { enabled: setting.enabled, clientId: setting.clientId, clientSecret: "", authorizationUrl: setting.authorizationUrl, tokenUrl: setting.tokenUrl, userInfoUrl: setting.userInfoUrl, redirectUrl: setting.redirectUrl, scopes: setting.scopes || [], clientAuthMethod: setting.clientAuthMethod, subjectField: setting.subjectField, usernameField: setting.usernameField, displayNameField: setting.displayNameField, emailField: setting.emailField, avatarField: setting.avatarField, }; } function normalizeLinuxDOFormValues(values: LinuxDOFormValues): LinuxDOFormValues { return { enabled: Boolean(values.enabled), clientId: values.clientId?.trim() || "", clientSecret: values.clientSecret?.trim() || "", authorizationUrl: values.authorizationUrl?.trim() || "", tokenUrl: values.tokenUrl?.trim() || "", userInfoUrl: values.userInfoUrl?.trim() || "", redirectUrl: values.redirectUrl?.trim() || "", scopes: [...new Set((values.scopes || []).map((value) => value.trim()).filter(Boolean))], clientAuthMethod: values.clientAuthMethod || "client_secret_post", subjectField: values.subjectField?.trim() || "id", usernameField: values.usernameField?.trim() || "username", displayNameField: values.displayNameField?.trim() || "name", emailField: values.emailField?.trim() || "email", avatarField: values.avatarField?.trim() || "avatar_url", }; } function hasLinuxDOChanges(values: LinuxDOFormValues, setting: LinuxDOSetting | null) { if (!setting) return false; const draft = normalizeLinuxDOFormValues(values); const saved = normalizeLinuxDOFormValues(toLinuxDOFormValues(setting)); if (draft.clientSecret) return true; return (Object.keys(saved) as Array).some((key) => key !== "clientSecret" && JSON.stringify(draft[key]) !== JSON.stringify(saved[key])); } function validateLinuxDODraft(values: LinuxDOFormValues, setting: LinuxDOSetting | null) { const draft = normalizeLinuxDOFormValues(values); if (!draft.enabled) return ""; if (!draft.clientId || (!draft.clientSecret && !setting?.hasClientSecret) || !draft.authorizationUrl || !draft.tokenUrl || !draft.userInfoUrl || !draft.redirectUrl) return "启用 Linux.do 登录前请完整填写 Client、端点和回调配置"; for (const value of [draft.authorizationUrl, draft.tokenUrl, draft.userInfoUrl]) if (!isValidURL(value, true)) return "Linux.do 授权、Token 和用户资料地址必须是有效的 HTTPS URL"; if (!isValidRedirectURL(draft.redirectUrl)) return "Linux.do 回调地址必须使用 HTTPS,本地回环地址可使用 HTTP"; return ""; } function linuxDOResponseMatches(setting: LinuxDOSetting, expected: LinuxDOFormValues) { const actual = normalizeLinuxDOFormValues(toLinuxDOFormValues(setting)); const fields: Array = ["enabled", "clientId", "authorizationUrl", "tokenUrl", "userInfoUrl", "redirectUrl", "scopes", "clientAuthMethod", "subjectField", "usernameField", "displayNameField", "emailField", "avatarField"]; if (expected.clientSecret && !setting.hasClientSecret) return false; return fields.every((key) => JSON.stringify(actual[key]) === JSON.stringify(expected[key])); } function isValidURL(value: string, requireHTTPS = false) { try { const parsed = new URL(value); return Boolean(parsed.host) && (!requireHTTPS || parsed.protocol === "https:"); } catch { return false; } } function isValidRedirectURL(value: string) { try { const parsed = new URL(value); if (parsed.protocol === "https:" && parsed.host) return true; return parsed.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname); } catch { return false; } } function formatSettingTime(value: string | undefined, fallback: string) { if (!value) return fallback; const date = new Date(value); if (Number.isNaN(date.getTime()) || date.getFullYear() < 2000) return fallback; return `更新于 ${date.toLocaleString("zh-CN", { hour12: false })}`; }