import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Card, Col, ConfigProvider, FloatButton, Layout, Modal, Row, Space, Spin, Tabs, Tooltip, message, } from 'antd'; import { CloudServerOutlined, CodeOutlined, MessageOutlined, SafetyOutlined, SettingOutlined, } from '@ant-design/icons'; import { HttpUtil, PromiseUtil } from '@/utils'; import { setMessageInstance } from '@/utils/messageBus'; import { useTheme } from '@/hooks/useTheme'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useAllSetting } from '@/hooks/useAllSetting'; import AppSidebar from '@/components/AppSidebar'; import GeneralTab from './GeneralTab'; import SecurityTab from './SecurityTab'; import TelegramTab from './TelegramTab'; import SubscriptionGeneralTab from './SubscriptionGeneralTab'; import SubscriptionFormatsTab from './SubscriptionFormatsTab'; import '@/styles/page-cards.css'; import './SettingsPage.css'; interface ApiMsg { success?: boolean; } const basePath = window.X_UI_BASE_PATH || ''; const requestUri = window.location.pathname; const tabSlugs = ['general', 'security', 'telegram', 'subscription', 'subscription-formats']; function slugToKey(slug: string): string { const i = tabSlugs.indexOf(slug); return i >= 0 ? String(i + 1) : '1'; } function keyToSlug(key: string): string { return tabSlugs[Number(key) - 1] || tabSlugs[0]; } function isIp(h: string): boolean { if (typeof h !== 'string') return false; const v4 = h.split('.'); if (v4.length === 4 && v4.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255)) return true; if (!h.includes(':') || h.includes(':::')) return false; const parts = h.split('::'); if (parts.length > 2) return false; const split = (s: string) => (s ? s.split(':').filter(Boolean) : []); const head = split(parts[0]); const tail = split(parts[1]); const valid = (seg: string) => /^[0-9a-fA-F]{1,4}$/.test(seg); if (![...head, ...tail].every(valid)) return false; const groups = head.length + tail.length; return parts.length === 2 ? groups < 8 : groups === 8; } function scrollTarget() { return document.getElementById('content-layout') as HTMLElement; } export default function SettingsPage() { const { t } = useTranslation(); const { isDark, isUltra, antdThemeConfig } = useTheme(); const { isMobile } = useMediaQuery(); const [modal, modalContextHolder] = Modal.useModal(); const [messageApi, messageContextHolder] = message.useMessage(); useEffect(() => { setMessageInstance(messageApi); }, [messageApi]); const { allSetting, updateSetting, fetched, spinning, setSpinning, saveDisabled, saveAll, } = useAllSetting(); const [entryHost, setEntryHost] = useState(''); const [entryPort, setEntryPort] = useState(''); const [entryIsIP, setEntryIsIP] = useState(false); useEffect(() => { const host = window.location.hostname; setEntryHost(host); setEntryPort(window.location.port); setEntryIsIP(isIp(host)); }, []); const [alertVisible, setAlertVisible] = useState(true); const [activeTabKey, setActiveTabKey] = useState(() => slugToKey(window.location.hash.slice(1))); useEffect(() => { const onHashChange = () => setActiveTabKey(slugToKey(window.location.hash.slice(1))); window.addEventListener('hashchange', onHashChange); return () => window.removeEventListener('hashchange', onHashChange); }, []); function onTabChange(key: string) { setActiveTabKey(key); const slug = keyToSlug(key); if (window.location.hash !== `#${slug}`) { history.replaceState(null, '', `#${slug}`); } } function rebuildUrlAfterRestart(): string { const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = allSetting; const newProtocol = (webCertFile || webKeyFile) ? 'https:' : 'http:'; let base = webBasePath ? webBasePath.replace(/^\//, '') : ''; if (base && !base.endsWith('/')) base += '/'; if (!entryIsIP) { const url = new URL(window.location.href); url.pathname = `/${base}panel/settings`; url.protocol = newProtocol; return url.toString(); } let finalHost = entryHost; let finalPort = entryPort || ''; if (webDomain && isIp(webDomain)) finalHost = webDomain; if (webPort && Number(webPort) !== Number(entryPort)) finalPort = String(webPort); const url = new URL(`${newProtocol}//${finalHost}`); if (finalPort) url.port = finalPort; url.pathname = `/${base}panel/settings`; return url.toString(); } function restartPanel() { modal.confirm({ title: t('pages.settings.restartPanel'), content: t('pages.settings.restartPanelDesc'), okText: t('pages.settings.restartPanel'), okButtonProps: { danger: true }, cancelText: t('cancel'), onOk: async () => { setSpinning(true); try { const msg = await HttpUtil.post('/panel/setting/restartPanel') as ApiMsg; if (!msg?.success) return; await PromiseUtil.sleep(5000); window.location.replace(rebuildUrlAfterRestart()); } finally { setSpinning(false); } }, }); } const confAlerts = useMemo(() => { const out: string[] = []; if (window.location.protocol !== 'https:') { out.push('Panel is served over plain HTTP — set up TLS for production.'); } if (allSetting.webPort === 2053) { out.push('Default port 2053 is well-known — change it to a random port.'); } const segs = window.location.pathname.split('/').length < 4; if (segs && allSetting.webBasePath === '/') { out.push('Default base path "/" is well-known — change it to a random path.'); } if (allSetting.subEnable) { let subPath = allSetting.subPath; if (allSetting.subURI) { try { subPath = new URL(allSetting.subURI).pathname; } catch { /* noop */ } } if (subPath === '/sub/') { out.push('Default subscription path "/sub/" is well-known — change it.'); } } if (allSetting.subJsonEnable) { let p = allSetting.subJsonPath; if (allSetting.subJsonURI) { try { p = new URL(allSetting.subJsonURI).pathname; } catch { /* noop */ } } if (p === '/json/') { out.push('Default JSON subscription path "/json/" is well-known — change it.'); } } return out; }, [allSetting]); const pageClass = useMemo(() => { const classes = ['settings-page']; if (isDark) classes.push('is-dark'); if (isUltra) classes.push('is-ultra'); return classes.join(' '); }, [isDark, isUltra]); const tabItems = useMemo(() => { const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = [ { key: '1', label: ( {!isMobile && <> {t('pages.settings.panelSettings')}} ), children: , }, { key: '2', label: ( {!isMobile && <> {t('pages.settings.securitySettings')}} ), children: , }, { key: '3', label: ( {!isMobile && <> {t('pages.settings.TGBotSettings')}} ), children: , }, { key: '4', label: ( {!isMobile && <> {t('pages.settings.subSettings')}} ), children: , }, ]; if (allSetting.subJsonEnable || allSetting.subClashEnable) { items.push({ key: '5', label: ( {!isMobile && <> {t('pages.settings.subSettings')} (Formats)} ), children: , }); } return items; }, [allSetting, updateSetting, isMobile, t]); return ( {messageContextHolder} {modalContextHolder} {!fetched ? (
) : ( <> {confAlerts.length > 0 && alertVisible && ( setAlertVisible(false)} title="Security warnings" description={( <> Your panel may be exposed:
    {confAlerts.map((msg, i) =>
  • {msg}
  • )}
)} /> )} )} ); }