Add flexible IP access control to WebUI config

Replaces the 'disableNonLANAccess' option with a more flexible access control system supporting 'none', 'whitelist', and 'blacklist' modes, along with IP list and X-Forwarded-For support. Updates backend API, config schema, middleware, and frontend UI to allow configuration of access control mode, IP whitelist/blacklist, and X-Forwarded-For handling. Removes legacy LAN-only access logic and updates types accordingly.
This commit is contained in:
手瓜一十雪
2026-01-26 19:46:15 +08:00
parent 0d4ff2abe4
commit 5bc8d5ad68
9 changed files with 489 additions and 107 deletions

View File

@@ -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 <PageLoading loading />;
return (
@@ -161,19 +228,89 @@ const ServerConfigCard = () => {
/>
)}
/>
<Controller
control={control}
name='disableNonLANAccess'
render={({ field }) => (
<SwitchCard
value={field.value}
onValueChange={(value: boolean) => field.onChange(value)}
disabled={!!configError}
label='禁用非局域网访问'
description='启用后只允许局域网内的设备访问WebUI提高安全性'
/>
<div className='flex flex-col gap-3 mt-2'>
<div className='text-sm font-medium text-default-700 dark:text-default-300 px-1'>访</div>
<Controller
control={control}
name='accessControlMode'
render={({ field }) => (
<Select
{...field}
label='访问控制模式'
placeholder='选择访问控制模式'
description='选择如何控制网络访问'
selectedKeys={[field.value]}
onSelectionChange={(keys) => {
const value = Array.from(keys)[0] as 'none' | 'whitelist' | 'blacklist';
field.onChange(value);
}}
isDisabled={!!configError}
classNames={{
trigger:
'bg-default-100/50 dark:bg-white/5 backdrop-blur-md border border-transparent hover:bg-default-200/50 dark:hover:bg-white/10 transition-all shadow-sm data-[hover=true]:border-default-300',
}}
>
<SelectItem key='none' value='none'>
</SelectItem>
<SelectItem key='whitelist' value='whitelist'>
</SelectItem>
<SelectItem key='blacklist' value='blacklist'>
</SelectItem>
</Select>
)}
/>
{accessControlMode !== 'none' && (
<div className='flex flex-col gap-2'>
<div className='flex flex-col gap-1'>
<label className='text-sm font-medium text-default-700 dark:text-default-300'>
{accessControlMode === 'whitelist' ? 'IP白名单' : 'IP黑名单'}
</label>
<textarea
value={ipListText}
onChange={(e) => setIpListText(e.target.value)}
placeholder={`每行一个IP地址或规则\n支持格式\nIPv4:\n- 精确IP: 192.168.1.100\n- 通配符: 192.168.1.*\n- CIDR: 192.168.1.0/24\nIPv6:\n- 精确IP: 2001:db8::1\n- CIDR: 2001:db8::/32`}
disabled={!!configError}
rows={10}
className='w-full px-3 py-2 bg-default-100/50 dark:bg-white/5 backdrop-blur-md border border-transparent hover:bg-default-200/50 dark:hover:bg-white/10 transition-all shadow-sm hover:border-default-300 rounded-lg text-default-700 dark:text-default-300 placeholder:text-default-400 font-mono text-sm resize-y focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed'
/>
<p className='text-xs text-default-500'>
{accessControlMode === 'whitelist'
? '只有列表中的IP可以访问'
: '列表中的IP将被拒绝访问'}
</p>
</div>
<div className='text-xs text-default-500 px-1'>
<div className='font-medium mb-1'></div>
<div className='space-y-0.5 font-mono'>
<div> 127.0.0.1 (IPv4 )</div>
<div> 192.168.1.* (IPv4 )</div>
<div> 10.0.0.0/8 (IPv4 CIDR)</div>
<div> ::1 (IPv6 )</div>
<div> 2001:db8::/32 (IPv6 CIDR)</div>
</div>
</div>
</div>
)}
/>
<Controller
control={control}
name='enableXForwardedFor'
render={({ field }) => (
<SwitchCard
value={field.value}
onValueChange={(value: boolean) => field.onChange(value)}
disabled={!!configError}
label='启用 X-Forwarded-For'
description='启用后将从 X-Forwarded-For 头部获取真实IP地址适用于反向代理场景'
/>
)}
/>
</div>
</div>
</div>