import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react' import { Clock, Globe, Lock, LogIn, Monitor, RefreshCw, ShieldCheck, Upload, UserCog } from 'lucide-react' import { changePassword, changeUsername, getLoginLogs, getSSLSettings, LoginLog, SSLSettings, updateSSLSettings, } from '../services/api' import { useDialog } from '../components/Dialog' import { useAuth } from '../contexts/AuthContext' export default function Settings() { const dialog = useDialog() const { username } = useAuth() const [logs, setLogs] = useState([]) const [loading, setLoading] = useState(true) const [logPage, setLogPage] = useState(1) const pageSize = 10 const [oldPwd, setOldPwd] = useState('') const [newPwd, setNewPwd] = useState('') const [newUsername, setNewUsername] = useState('') const [ssl, setSSL] = useState(null) const [sslEnabled, setSSLEnabled] = useState(false) const [sslMode, setSSLMode] = useState('disabled') const [sslTarget, setSSLTarget] = useState('') const [sslEmail, setSSLEmail] = useState('') const [certPEM, setCertPEM] = useState('') const [keyPEM, setKeyPEM] = useState('') const [applyNow, setApplyNow] = useState(true) const [savingSSL, setSavingSSL] = useState(false) const fetchLogs = useCallback(async () => { try { const res = await getLoginLogs() if (res.data.data) setLogs(res.data.data) } catch (err) { console.error(err) } finally { setLoading(false) } }, []) const fetchSSL = useCallback(async () => { try { const res = await getSSLSettings() const data = res.data.data if (!data) return setSSL(data) setSSLEnabled(data.enabled) setSSLMode(data.mode || 'disabled') setSSLTarget(data.target || data.detected_host || '') setSSLEmail(data.email || '') } catch (err) { console.error(err) } }, []) useEffect(() => { fetchLogs() fetchSSL() const timer = setInterval(fetchLogs, 15000) return () => clearInterval(timer) }, [fetchLogs, fetchSSL]) const handleSSLModeChange = (mode: SSLSettings['mode']) => { setSSLMode(mode) const saved = ssl?.mode_certificates?.[mode] setSSLTarget(saved?.target || ssl?.detected_host || sslTarget) setSSLEmail(saved?.email || '') } const handleSaveSSL = async () => { setSavingSSL(true) try { const enabled = sslEnabled && sslMode !== 'disabled' const res = await updateSSLSettings({ enabled, mode: enabled ? sslMode : 'disabled', target: sslTarget, email: sslEmail, cert_pem: certPEM, key_pem: keyPEM, apply_now: applyNow, }) if (res.data.data) { setSSL(res.data.data) setCertPEM('') setKeyPEM('') } dialog.alert('完成', applyNow ? 'SSL 设置已保存,服务正在重启。稍后请用新的协议重新打开面板。' : 'SSL 设置已保存,重启 clicd 服务后生效。') } catch (err: unknown) { const e = err as { response?: { data?: { message?: string } } } dialog.alert('失败', e.response?.data?.message || 'SSL 设置保存失败') } finally { setSavingSSL(false) } } const handleSaveAccount = async () => { if (!oldPwd) { dialog.alert('提示', '请输入当前密码以确认修改') return } if (!newPwd && !newUsername) { dialog.alert('提示', '至少填写新密码或新用户名中的一项') return } if (newPwd && newPwd.length < 6) { dialog.alert('提示', '新密码至少 6 位') return } if (newUsername && newUsername.length < 3) { dialog.alert('提示', '用户名至少 3 位') return } const results: string[] = [] try { if (newUsername) { const res = await changeUsername(newUsername, oldPwd) results.push(res.data.success ? '用户名已修改' : '用户名修改失败') } if (newPwd) { const res = await changePassword(oldPwd, newPwd) results.push(res.data.success ? '密码已修改' : '密码修改失败') } if (results.length > 0) { dialog.alert('完成', `${results.join(',')}。下次登录生效`) setOldPwd('') setNewPwd('') setNewUsername('') } } catch (err: unknown) { const e = err as { response?: { data?: { message?: string } } } dialog.alert('失败', e.response?.data?.message || '修改失败') } } if (loading) { return (
) } const totalPages = Math.ceil(logs.length / pageSize) return (

面板设置

账号、安全证书与登录日志

账号设置

setNewUsername(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="至少 3 位" />
setNewPwd(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="至少 6 位" />
setOldPwd(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="输入当前密码以确认修改" />
) } interface SSLCardProps { ssl: SSLSettings | null sslEnabled: boolean sslMode: SSLSettings['mode'] sslTarget: string sslEmail: string certPEM: string keyPEM: string applyNow: boolean savingSSL: boolean onRefresh: () => void onEnabledChange: (enabled: boolean) => void onModeChange: (mode: SSLSettings['mode']) => void onTargetChange: (target: string) => void onEmailChange: (email: string) => void onCertChange: (cert: string) => void onKeyChange: (key: string) => void onApplyNowChange: (apply: boolean) => void onSave: () => void } function SSLCard(props: SSLCardProps) { const selectedSSL = props.ssl?.mode_certificates?.[props.sslMode] const modeOptions: Array<{ value: SSLSettings['mode']; label: string }> = [ { value: 'letsencrypt', label: 'Let’s Encrypt' }, { value: 'self_signed', label: '自签证书' }, { value: 'uploaded', label: '上传证书' }, ] return (

SSL 证书

{modeOptions.map((option) => ( ))}
props.onTargetChange(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder={props.ssl?.detected_host || '服务器公网 IP 或域名'} />
{props.sslMode === 'letsencrypt' && (
props.onEmailChange(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="admin@example.com" />
)}
{props.sslMode === 'letsencrypt' && (
纯 IP 证书需要服务器安装 Certbot 5.4+,且验证时 80 端口必须能被 Let’s Encrypt 访问。IP 证书是短有效期证书,certbot 需要保持自动续签。
)} {props.sslMode === 'self_signed' && (
自签证书可以加密面板和 VNC,但浏览器会提示证书不受信任;证书快到期时系统会自动重新签发。
)} {props.sslMode === 'uploaded' && (