diff --git a/packages/napcat-webui-backend/src/api/WebUIConfig.ts b/packages/napcat-webui-backend/src/api/WebUIConfig.ts index f1f42bce..12dd21b9 100644 --- a/packages/napcat-webui-backend/src/api/WebUIConfig.ts +++ b/packages/napcat-webui-backend/src/api/WebUIConfig.ts @@ -12,7 +12,10 @@ export const GetWebUIConfigHandler: RequestHandler = async (_, res) => { port: config.port, loginRate: config.loginRate, disableWebUI: config.disableWebUI, - disableNonLANAccess: config.disableNonLANAccess, + accessControlMode: config.accessControlMode || 'none', + ipWhitelist: config.ipWhitelist || [], + ipBlacklist: config.ipBlacklist || [], + enableXForwardedFor: config.enableXForwardedFor || false, }); } catch (error) { const msg = (error as Error).message; @@ -48,38 +51,47 @@ export const UpdateDisableWebUIHandler: RequestHandler = async (req, res) => { } }; -// 获取是否禁用非局域网访问 -export const GetDisableNonLANAccessHandler: RequestHandler = async (_, res) => { +// 获取当前客户端IP +export const GetClientIPHandler: RequestHandler = async (req, res) => { try { - const disable = await WebUiConfig.GetDisableNonLANAccess(); - return sendSuccess(res, disable); - } catch (error) { - const msg = (error as Error).message; - return sendError(res, `获取非局域网访问禁用状态失败: ${msg}`); - } -}; + const config = await WebUiConfig.GetWebUIConfig(); -// 更新是否禁用非局域网访问 -export const UpdateDisableNonLANAccessHandler: RequestHandler = async (req, res) => { - try { - const { disable } = req.body; - - if (typeof disable !== 'boolean') { - return sendError(res, 'disable参数必须是布尔值'); + // 根据配置决定如何获取客户端IP(与 CORS 中间件逻辑一致) + let clientIP: string; + if (config.enableXForwardedFor) { + const forwardedFor = req.headers['x-forwarded-for']; + if (typeof forwardedFor === 'string') { + clientIP = forwardedFor.split(',')[0]?.trim() || ''; + } else if (Array.isArray(forwardedFor) && forwardedFor.length > 0) { + clientIP = forwardedFor[0] || ''; + } else { + clientIP = req.ip || req.socket.remoteAddress || ''; + } + } else { + clientIP = req.ip || req.socket.remoteAddress || ''; } - await WebUiConfig.UpdateDisableNonLANAccess(disable); - return sendSuccess(res, null); + // 标准化 IP(移除 IPv4-mapped IPv6 前缀,但保留纯 IPv6) + let normalizedIP = clientIP; + if (clientIP.startsWith('::ffff:')) { + const ipv4 = clientIP.substring(7); + // 检查是否是有效的 IPv4 + if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ipv4)) { + normalizedIP = ipv4; + } + } + + return sendSuccess(res, { ip: normalizedIP }); } catch (error) { const msg = (error as Error).message; - return sendError(res, `更新非局域网访问禁用状态失败: ${msg}`); + return sendError(res, `获取客户端IP失败: ${msg}`); } }; // 更新WebUI基础配置 export const UpdateWebUIConfigHandler: RequestHandler = async (req, res) => { try { - const { host, port, loginRate, disableWebUI, disableNonLANAccess } = req.body; + const { host, port, loginRate, disableWebUI, accessControlMode, ipWhitelist, ipBlacklist, enableXForwardedFor } = req.body; const updateConfig: any = {}; @@ -111,11 +123,32 @@ export const UpdateWebUIConfigHandler: RequestHandler = async (req, res) => { updateConfig.disableWebUI = disableWebUI; } - if (disableNonLANAccess !== undefined) { - if (typeof disableNonLANAccess !== 'boolean') { - return sendError(res, 'disableNonLANAccess必须是布尔值'); + if (accessControlMode !== undefined) { + if (!['none', 'whitelist', 'blacklist'].includes(accessControlMode)) { + return sendError(res, 'accessControlMode必须是none、whitelist或blacklist'); } - updateConfig.disableNonLANAccess = disableNonLANAccess; + updateConfig.accessControlMode = accessControlMode; + } + + if (ipWhitelist !== undefined) { + if (!Array.isArray(ipWhitelist)) { + return sendError(res, 'ipWhitelist必须是数组'); + } + updateConfig.ipWhitelist = ipWhitelist; + } + + if (ipBlacklist !== undefined) { + if (!Array.isArray(ipBlacklist)) { + return sendError(res, 'ipBlacklist必须是数组'); + } + updateConfig.ipBlacklist = ipBlacklist; + } + + if (enableXForwardedFor !== undefined) { + if (typeof enableXForwardedFor !== 'boolean') { + return sendError(res, 'enableXForwardedFor必须是布尔值'); + } + updateConfig.enableXForwardedFor = enableXForwardedFor; } await WebUiConfig.UpdateWebUIConfig(updateConfig); diff --git a/packages/napcat-webui-backend/src/helper/config.ts b/packages/napcat-webui-backend/src/helper/config.ts index 3faa4285..864761a9 100644 --- a/packages/napcat-webui-backend/src/helper/config.ts +++ b/packages/napcat-webui-backend/src/helper/config.ts @@ -20,8 +20,18 @@ const WebUiConfigSchema = Type.Object({ theme: themeType, // 是否关闭WebUI disableWebUI: Type.Boolean({ default: false }), - // 是否关闭非局域网访问 - disableNonLANAccess: Type.Boolean({ default: false }), + // 网络访问控制模式: 'none' | 'whitelist' | 'blacklist' + accessControlMode: Type.Union([ + Type.Literal('none'), + Type.Literal('whitelist'), + Type.Literal('blacklist'), + ], { default: 'none' }), + // IP白名单列表 + ipWhitelist: Type.Array(Type.String(), { default: [] }), + // IP黑名单列表 + ipBlacklist: Type.Array(Type.String(), { default: [] }), + // 是否启用 X-Forwarded-For 获取真实IP + enableXForwardedFor: Type.Boolean({ default: false }), }); export type WebUiConfigType = Static; @@ -245,14 +255,47 @@ export class WebUiConfigWrapper { await this.UpdateWebUIConfig({ disableWebUI: disable }); } - // 获取是否禁用非局域网访问 - async GetDisableNonLANAccess (): Promise { + // 获取访问控制模式 + async GetAccessControlMode (): Promise<'none' | 'whitelist' | 'blacklist'> { const config = await this.GetWebUIConfig(); - return config.disableNonLANAccess; + return config.accessControlMode; } - // 更新是否禁用非局域网访问 - async UpdateDisableNonLANAccess (disable: boolean): Promise { - await this.UpdateWebUIConfig({ disableNonLANAccess: disable }); + // 更新访问控制模式 + async UpdateAccessControlMode (mode: 'none' | 'whitelist' | 'blacklist'): Promise { + await this.UpdateWebUIConfig({ accessControlMode: mode }); + } + + // 获取IP白名单 + async GetIpWhitelist (): Promise { + const config = await this.GetWebUIConfig(); + return config.ipWhitelist; + } + + // 更新IP白名单 + async UpdateIpWhitelist (whitelist: string[]): Promise { + await this.UpdateWebUIConfig({ ipWhitelist: whitelist }); + } + + // 获取IP黑名单 + async GetIpBlacklist (): Promise { + const config = await this.GetWebUIConfig(); + return config.ipBlacklist; + } + + // 更新IP黑名单 + async UpdateIpBlacklist (blacklist: string[]): Promise { + await this.UpdateWebUIConfig({ ipBlacklist: blacklist }); + } + + // 获取是否启用 X-Forwarded-For + async GetEnableXForwardedFor (): Promise { + const config = await this.GetWebUIConfig(); + return config.enableXForwardedFor || false; + } + + // 更新是否启用 X-Forwarded-For + async UpdateEnableXForwardedFor (enable: boolean): Promise { + await this.UpdateWebUIConfig({ enableXForwardedFor: enable }); } } diff --git a/packages/napcat-webui-backend/src/middleware/cors.ts b/packages/napcat-webui-backend/src/middleware/cors.ts index ff212297..062f4b7c 100644 --- a/packages/napcat-webui-backend/src/middleware/cors.ts +++ b/packages/napcat-webui-backend/src/middleware/cors.ts @@ -1,52 +1,223 @@ import type { RequestHandler } from 'express'; import { WebUiConfig } from '@/napcat-webui-backend/index'; -// 检查是否为局域网IP地址 -function isLANIP (ip: string): boolean { - if (!ip) return false; +// 标准化 IP 地址(移除 IPv6 前缀,但保留完整的 IPv6 地址) +function normalizeIP (ip: string): string { + // 移除 IPv4-mapped IPv6 前缀 + if (ip.startsWith('::ffff:')) { + const ipv4 = ip.substring(7); + // 检查是否是有效的 IPv4 + if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ipv4)) { + return ipv4; + } + } + return ip; +} - // 移除IPv6的前缀,如果存在 - const cleanIP = ip.replace(/^::ffff:/, ''); +// 检查是否是 IPv6 地址 +function isIPv6 (ip: string): boolean { + return ip.includes(':'); +} - // 本地回环地址 - if (cleanIP === '127.0.0.1' || cleanIP === 'localhost' || cleanIP === '::1') { - return true; +// 检查 IP 是否匹配规则(支持 IPv4 和 IPv6) +function matchIPRule (ip: string, rule: string): boolean { + const cleanIP = normalizeIP(ip); + const cleanRule = normalizeIP(rule); + + // 精确匹配 + if (cleanIP === cleanRule) return true; + + // IPv6 地址不支持通配符,只支持 CIDR + if (isIPv6(cleanIP) || isIPv6(cleanRule)) { + if (cleanRule.includes('/')) { + return matchIPv6CIDR(cleanIP, cleanRule); + } + return false; } - // 检查IPv4私有网络地址 - const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; - const match = cleanIP.match(ipv4Regex); + // IPv4 通配符匹配 (例如: 192.168.*.* 或 192.168.1.*) + if (cleanRule.includes('*')) { + const ruleRegex = new RegExp('^' + cleanRule.replace(/\./g, '\\.').replace(/\*/g, '\\d+') + '$'); + return ruleRegex.test(cleanIP); + } - if (match) { - const [, a, b] = match.map(Number); - - // 10.0.0.0/8 - if (a === 10) return true; - - // 172.16.0.0/12 - if (a === 172 && b !== undefined && b >= 16 && b <= 31) return true; - - // 192.168.0.0/16 - if (a === 192 && b === 168) return true; - - // 169.254.0.0/16 (链路本地地址) - if (a === 169 && b === 254) return true; + // IPv4 CIDR 匹配 (例如: 192.168.1.0/24) + if (cleanRule.includes('/')) { + return matchIPv4CIDR(cleanIP, cleanRule); } return false; } +// IPv4 CIDR 匹配 +function matchIPv4CIDR (ip: string, cidr: string): boolean { + const parts = cidr.split('/'); + if (parts.length !== 2) return false; + + const range = parts[0]; + const bits = parts[1]; + if (!range || !bits) return false; + + const prefixLength = parseInt(bits); + if (isNaN(prefixLength) || prefixLength < 0 || prefixLength > 32) return false; + + const mask = prefixLength === 0 ? 0 : ~(2 ** (32 - prefixLength) - 1); + + const ipNum = ipv4ToNumber(ip); + const rangeNum = ipv4ToNumber(range); + + if (ipNum === null || rangeNum === null) return false; + + return (ipNum & mask) === (rangeNum & mask); +} + +// IPv4 转数字 +function ipv4ToNumber (ip: string): number | null { + const parts = ip.split('.'); + if (parts.length !== 4) return null; + + const nums = parts.map(p => parseInt(p)); + if (nums.some(n => isNaN(n) || n < 0 || n > 255)) return null; + + return ((nums[0] || 0) << 24) + ((nums[1] || 0) << 16) + ((nums[2] || 0) << 8) + (nums[3] || 0); +} + +// IPv6 CIDR 匹配 +function matchIPv6CIDR (ip: string, cidr: string): boolean { + const parts = cidr.split('/'); + if (parts.length !== 2) return false; + + const range = parts[0]; + const bits = parts[1]; + if (!range || !bits) return false; + + const prefixLength = parseInt(bits); + if (isNaN(prefixLength) || prefixLength < 0 || prefixLength > 128) return false; + + try { + const ipSegments = expandIPv6(ip); + const rangeSegments = expandIPv6(range); + + if (!ipSegments || !rangeSegments) return false; + + // 按位比较 + let bitsToCompare = prefixLength; + for (let i = 0; i < 8; i++) { + if (bitsToCompare <= 0) break; + + const ipSeg = ipSegments[i] ?? 0; + const rangeSeg = rangeSegments[i] ?? 0; + + if (bitsToCompare >= 16) { + // 完整比较这个段 + if (ipSeg !== rangeSeg) return false; + bitsToCompare -= 16; + } else { + // 部分比较这个段 + const mask = (0xFFFF << (16 - bitsToCompare)) & 0xFFFF; + if ((ipSeg & mask) !== (rangeSeg & mask)) return false; + break; + } + } + + return true; + } catch (error) { + return false; + } +} + +// 展开 IPv6 地址为 8 个 16 位段 +function expandIPv6 (ip: string): number[] | null { + try { + // 移除可能的 IPv4 映射部分 + let addr = ip; + + // 处理 IPv4-mapped IPv6 (如 ::ffff:192.168.1.1) + const ipv4Match = addr.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/i); + if (ipv4Match && ipv4Match[1]) { + const ipv4 = ipv4Match[1]; + const ipv4Parts = ipv4.split('.').map(p => parseInt(p)); + if (ipv4Parts.length === 4 && ipv4Parts.every(n => n >= 0 && n <= 255)) { + // 转换为 IPv6 格式 + const part1 = (ipv4Parts[0] ?? 0) << 8 | (ipv4Parts[1] ?? 0); + const part2 = (ipv4Parts[2] ?? 0) << 8 | (ipv4Parts[3] ?? 0); + addr = `::ffff:${part1.toString(16)}:${part2.toString(16)}`; + } + } + + // 处理 :: 缩写 + if (addr.includes('::')) { + const sides = addr.split('::'); + if (sides.length > 2) return null; + + const left = sides[0] ? sides[0].split(':') : []; + const right = sides[1] ? sides[1].split(':') : []; + const missing = 8 - left.length - right.length; + + if (missing < 0) return null; + + const segments = [ + ...left, + ...Array(missing).fill('0'), + ...right, + ]; + + return segments.map(s => parseInt(s || '0', 16)); + } + + // 标准格式 + const segments = addr.split(':'); + if (segments.length !== 8) return null; + + return segments.map(s => parseInt(s || '0', 16)); + } catch (error) { + return null; + } +} + +// 检查是否在 IP 列表中 +function isIPInList (ip: string, ipList: string[]): boolean { + return ipList.some(rule => matchIPRule(ip, rule)); +} + // CORS 中间件,跨域用 export const cors: RequestHandler = async (req, res, next) => { - // 检查是否禁用非局域网访问 const config = await WebUiConfig.GetWebUIConfig(); - if (config.disableNonLANAccess) { - const clientIP = req.ip || req.socket.remoteAddress || ''; - if (!isLANIP(clientIP)) { - res.status(403).json({ error: '非局域网访问被禁止' }); + + // 根据配置决定如何获取客户端IP + let clientIP: string; + if (config.enableXForwardedFor) { + // 启用 X-Forwarded-For 时,优先从该头部获取真实IP + const forwardedFor = req.headers['x-forwarded-for']; + if (typeof forwardedFor === 'string') { + // X-Forwarded-For 可能包含多个IP,取第一个 + clientIP = forwardedFor.split(',')[0]?.trim() || ''; + } else if (Array.isArray(forwardedFor) && forwardedFor.length > 0) { + clientIP = forwardedFor[0] || ''; + } else { + // 如果没有 X-Forwarded-For,回退到 socket IP + clientIP = req.ip || req.socket.remoteAddress || ''; + } + } else { + // 默认使用 socket IP + clientIP = req.ip || req.socket.remoteAddress || ''; + } + + // 检查访问控制模式 + if (config.accessControlMode === 'whitelist') { + // 白名单模式:只允许白名单中的IP访问 + if (!isIPInList(clientIP, config.ipWhitelist || [])) { + res.status(403).json({ error: '访问被拒绝:IP不在白名单中' }); + return; + } + } else if (config.accessControlMode === 'blacklist') { + // 黑名单模式:拒绝黑名单中的IP访问 + if (isIPInList(clientIP, config.ipBlacklist || [])) { + res.status(403).json({ error: '访问被拒绝:IP在黑名单中' }); return; } } + // 'none' 模式:不进行任何限制 const origin = req.headers.origin || '*'; res.header('Access-Control-Allow-Origin', origin); diff --git a/packages/napcat-webui-backend/src/router/WebUIConfig.ts b/packages/napcat-webui-backend/src/router/WebUIConfig.ts index 225e7e9c..9ea7c432 100644 --- a/packages/napcat-webui-backend/src/router/WebUIConfig.ts +++ b/packages/napcat-webui-backend/src/router/WebUIConfig.ts @@ -3,9 +3,8 @@ import { GetWebUIConfigHandler, GetDisableWebUIHandler, UpdateDisableWebUIHandler, - GetDisableNonLANAccessHandler, - UpdateDisableNonLANAccessHandler, UpdateWebUIConfigHandler, + GetClientIPHandler, } from '@/napcat-webui-backend/src/api/WebUIConfig'; const router: Router = Router(); @@ -22,10 +21,7 @@ router.get('/GetDisableWebUI', GetDisableWebUIHandler); // 更新是否禁用WebUI router.post('/UpdateDisableWebUI', UpdateDisableWebUIHandler); -// 获取是否禁用非局域网访问 -router.get('/GetDisableNonLANAccess', GetDisableNonLANAccessHandler); - -// 更新是否禁用非局域网访问 -router.post('/UpdateDisableNonLANAccess', UpdateDisableNonLANAccessHandler); +// 获取当前客户端IP +router.get('/GetClientIP', GetClientIPHandler); export { router as WebUIConfigRouter }; diff --git a/packages/napcat-webui-backend/src/types/index.ts b/packages/napcat-webui-backend/src/types/index.ts index 6008dcd3..96f09fe2 100644 --- a/packages/napcat-webui-backend/src/types/index.ts +++ b/packages/napcat-webui-backend/src/types/index.ts @@ -20,6 +20,10 @@ export interface WebUiConfigType { port: number; token: string; loginRate: number; + accessControlMode?: 'none' | 'whitelist' | 'blacklist'; + ipWhitelist?: string[]; + ipBlacklist?: string[]; + enableXForwardedFor?: boolean; } export interface WebUiCredentialInnerJson { CreatedTime: number; diff --git a/packages/napcat-webui-backend/webui.json b/packages/napcat-webui-backend/webui.json index a3573a05..5d74c5ee 100644 --- a/packages/napcat-webui-backend/webui.json +++ b/packages/napcat-webui-backend/webui.json @@ -1,7 +1,11 @@ { - "host": "0.0.0.0", - "port": 6099, - "prefix": "", - "token": "random", - "loginRate": 3 -} + "host": "0.0.0.0", + "port": 6099, + "prefix": "", + "token": "random", + "loginRate": 3, + "accessControlMode": "none", + "ipWhitelist": [], + "ipBlacklist": [], + "enableXForwardedFor": false +} \ No newline at end of file diff --git a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts index 695d0cb6..c283d31c 100644 --- a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts @@ -273,19 +273,10 @@ export default class WebUIManager { return data.data; } - // 获取是否禁用非局域网访问 - public static async getDisableNonLANAccess () { - const { data } = await serverRequest.get>( - '/WebUIConfig/GetDisableNonLANAccess' - ); - return data.data; - } - - // 更新是否禁用非局域网访问 - public static async updateDisableNonLANAccess (disable: boolean) { - const { data } = await serverRequest.post>( - '/WebUIConfig/UpdateDisableNonLANAccess', - { disable } + // 获取当前客户端IP + public static async getClientIP () { + const { data } = await serverRequest.get>( + '/WebUIConfig/GetClientIP' ); return data.data; } diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/server.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/server.tsx index b1a6e20d..ba5e224b 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/server.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/server.tsx @@ -1,8 +1,9 @@ import { Input } from '@heroui/input'; import { useRequest } from 'ahooks'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; +import { Select, SelectItem } from '@heroui/select'; import SaveButtons from '@/components/button/save_buttons'; import PageLoading from '@/components/page_loading'; @@ -18,40 +19,73 @@ const ServerConfigCard = () => { refreshAsync: refreshConfig, } = useRequest(WebUIManager.getWebUIConfig); + const [ipListText, setIpListText] = useState(''); + const { control, handleSubmit: handleConfigSubmit, formState: { isSubmitting }, setValue: setConfigValue, + watch, } = useForm<{ host: string; port: number; loginRate: number; disableWebUI: boolean; - disableNonLANAccess: boolean; + accessControlMode: 'none' | 'whitelist' | 'blacklist'; + ipWhitelist: string[]; + ipBlacklist: string[]; + enableXForwardedFor: boolean; }>({ defaultValues: { host: '0.0.0.0', port: 6099, loginRate: 10, disableWebUI: false, - disableNonLANAccess: false, + accessControlMode: 'none', + ipWhitelist: [], + ipBlacklist: [], + enableXForwardedFor: false, }, }); + const accessControlMode = watch('accessControlMode'); + const reset = () => { if (configData) { setConfigValue('host', configData.host); setConfigValue('port', configData.port); setConfigValue('loginRate', configData.loginRate); setConfigValue('disableWebUI', configData.disableWebUI); - setConfigValue('disableNonLANAccess', configData.disableNonLANAccess); + setConfigValue('accessControlMode', configData.accessControlMode || 'none'); + setConfigValue('ipWhitelist', configData.ipWhitelist || []); + setConfigValue('ipBlacklist', configData.ipBlacklist || []); + setConfigValue('enableXForwardedFor', configData.enableXForwardedFor || false); + + // 更新IP列表文本 + if (configData.accessControlMode === 'whitelist') { + setIpListText((configData.ipWhitelist || []).join('\n')); + } else if (configData.accessControlMode === 'blacklist') { + setIpListText((configData.ipBlacklist || []).join('\n')); + } } }; const onSubmit = handleConfigSubmit(async (data) => { try { - await WebUIManager.updateWebUIConfig(data); + // 解析IP列表 + const ipList = ipListText + .split('\n') + .map(ip => ip.trim()) + .filter(ip => ip.length > 0); + + const submitData = { + ...data, + ipWhitelist: data.accessControlMode === 'whitelist' ? ipList : [], + ipBlacklist: data.accessControlMode === 'blacklist' ? ipList : [], + }; + + await WebUIManager.updateWebUIConfig(submitData); toast.success('保存成功'); } catch (error) { const msg = (error as Error).message; @@ -73,6 +107,39 @@ const ServerConfigCard = () => { reset(); }, [configData]); + useEffect(() => { + // 当模式切换时,更新IP列表文本 + const handleModeChange = async () => { + if (configData) { + if (accessControlMode === 'whitelist') { + const currentList = configData.ipWhitelist || []; + // 如果白名单为空,自动获取当前IP并填入 + if (currentList.length === 0) { + try { + const clientIPData = await WebUIManager.getClientIP(); + if (clientIPData?.ip) { + setIpListText(clientIPData.ip); + } else { + setIpListText(''); + } + } catch (error) { + console.error('获取客户端IP失败:', error); + setIpListText(''); + } + } else { + setIpListText(currentList.join('\n')); + } + } else if (accessControlMode === 'blacklist') { + setIpListText((configData.ipBlacklist || []).join('\n')); + } else { + setIpListText(''); + } + } + }; + + handleModeChange(); + }, [accessControlMode, configData]); + if (configLoading) return ; return ( @@ -161,19 +228,89 @@ const ServerConfigCard = () => { /> )} /> - ( - field.onChange(value)} - disabled={!!configError} - label='禁用非局域网访问' - description='启用后只允许局域网内的设备访问WebUI,提高安全性' - /> + +
+
网络访问控制
+ ( + + )} + /> + + {accessControlMode !== 'none' && ( +
+
+ +