mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
merge: 实装 PoW 配置面板
This commit is contained in:
@@ -45,11 +45,11 @@ const authSchema = z
|
||||
.superRefine((value, context) => {
|
||||
if (value.auth_mode === 'pow') {
|
||||
const difficulty = Number(value.pow_difficulty);
|
||||
if (!Number.isFinite(difficulty) || difficulty < 1 || difficulty > 10) {
|
||||
if (!Number.isFinite(difficulty) || difficulty < 1 || difficulty > 16) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['pow_difficulty'],
|
||||
message: '难度需为 1-10 的整数',
|
||||
message: '难度需为 1-16 的整数',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,18 +102,14 @@ function resolveAuthMode(route: ProxyRouteItem): AuthValues['auth_mode'] {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
function buildPowConfig(
|
||||
values: AuthValues,
|
||||
route: ProxyRouteItem,
|
||||
currentConfig: ProxyRoutePoWConfig,
|
||||
): ProxyRoutePoWConfig {
|
||||
return {
|
||||
...currentConfig,
|
||||
difficulty: Number(values.pow_difficulty),
|
||||
algorithm: values.pow_algorithm,
|
||||
session_ttl: Number(values.pow_session_ttl),
|
||||
challenge_ttl: Number(values.pow_challenge_ttl),
|
||||
};
|
||||
function syncPowFormValues(
|
||||
setValue: ReturnType<typeof useForm<AuthValues>>['setValue'],
|
||||
config: ProxyRoutePoWConfig,
|
||||
) {
|
||||
setValue('pow_difficulty', String(config.difficulty));
|
||||
setValue('pow_algorithm', config.algorithm);
|
||||
setValue('pow_session_ttl', String(config.session_ttl));
|
||||
setValue('pow_challenge_ttl', String(config.challenge_ttl));
|
||||
}
|
||||
|
||||
interface AuthSectionProps {
|
||||
@@ -171,7 +167,8 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
|
||||
id={proxyRouteFormIds.auth}
|
||||
className="space-y-5"
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
const nextPowConfig = buildPowConfig(values, route, powConfig);
|
||||
const nextPowConfig =
|
||||
values.auth_mode === 'pow' ? powConfig : route.pow_config ?? defaultPowConfig;
|
||||
const powEnabled = values.auth_mode === 'pow' || values.pow_enabled;
|
||||
|
||||
await save(
|
||||
@@ -247,82 +244,15 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
|
||||
) : null}
|
||||
|
||||
{authMode === 'pow' ? (
|
||||
<div className="space-y-4 rounded-lg border border-dashed p-4">
|
||||
<PowConfigPanel
|
||||
enabled={form.watch('pow_enabled')}
|
||||
config={powConfig}
|
||||
onChange={(enabled, config) => {
|
||||
form.setValue('pow_enabled', enabled);
|
||||
setPowConfig(config);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pow_difficulty"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>难度</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="4" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pow_algorithm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>算法</FormLabel>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="fast">fast</SelectItem>
|
||||
<SelectItem value="slow">slow</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pow_session_ttl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>会话 TTL (秒)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="600" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pow_challenge_ttl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>挑战 TTL (秒)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="300" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PowConfigPanel
|
||||
enabled={form.watch('pow_enabled')}
|
||||
config={powConfig}
|
||||
onChange={(enabled, config) => {
|
||||
form.setValue('pow_enabled', enabled);
|
||||
setPowConfig(config);
|
||||
syncPowFormValues(form.setValue, config);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,43 +1,244 @@
|
||||
'use client';
|
||||
|
||||
import {Info} from 'lucide-react';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {Switch} from '@/components/ui/switch';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {Switch} from '@/components/ui/switch';
|
||||
import {Textarea} from '@/components/ui/textarea';
|
||||
import type {ProxyRoutePoWConfig} from '@/lib/services/openflare';
|
||||
|
||||
import {listToText, parseTextareaList} from './helpers';
|
||||
|
||||
interface PowConfigPanelProps {
|
||||
enabled: boolean;
|
||||
config: ProxyRoutePoWConfig;
|
||||
onChange: (enabled: boolean, config: ProxyRoutePoWConfig) => void;
|
||||
}
|
||||
|
||||
export function PowConfigPanel({ enabled, config, onChange }: PowConfigPanelProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-3 rounded-lg border border-dashed bg-muted/40 p-4 text-sm">
|
||||
<Info className="size-4 shrink-0 text-primary mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium">PoW 配置面板(占位)</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
完整 PoW 黑白名单与难度参数编辑将在后续迭代中实装。当前可先切换启用状态并保存规则组。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
type PowListDraft = {
|
||||
whitelist: Record<keyof ProxyRoutePoWConfig['whitelist'], string>;
|
||||
blacklist: Record<keyof ProxyRoutePoWConfig['blacklist'], string>;
|
||||
};
|
||||
|
||||
function buildDraft(config: ProxyRoutePoWConfig): PowListDraft {
|
||||
return {
|
||||
whitelist: {
|
||||
ips: listToText(config.whitelist?.ips),
|
||||
ip_cidrs: listToText(config.whitelist?.ip_cidrs),
|
||||
paths: listToText(config.whitelist?.paths),
|
||||
path_regexes: listToText(config.whitelist?.path_regexes),
|
||||
user_agents: listToText(config.whitelist?.user_agents),
|
||||
},
|
||||
blacklist: {
|
||||
ips: listToText(config.blacklist?.ips),
|
||||
ip_cidrs: listToText(config.blacklist?.ip_cidrs),
|
||||
paths: listToText(config.blacklist?.paths),
|
||||
path_regexes: listToText(config.blacklist?.path_regexes),
|
||||
user_agents: listToText(config.blacklist?.user_agents),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyDraft(
|
||||
config: ProxyRoutePoWConfig,
|
||||
draft: PowListDraft,
|
||||
): ProxyRoutePoWConfig {
|
||||
return {
|
||||
...config,
|
||||
whitelist: {
|
||||
ips: parseTextareaList(draft.whitelist.ips),
|
||||
ip_cidrs: parseTextareaList(draft.whitelist.ip_cidrs),
|
||||
paths: parseTextareaList(draft.whitelist.paths),
|
||||
path_regexes: parseTextareaList(draft.whitelist.path_regexes),
|
||||
user_agents: parseTextareaList(draft.whitelist.user_agents),
|
||||
},
|
||||
blacklist: {
|
||||
ips: parseTextareaList(draft.blacklist.ips),
|
||||
ip_cidrs: parseTextareaList(draft.blacklist.ip_cidrs),
|
||||
paths: parseTextareaList(draft.blacklist.paths),
|
||||
path_regexes: parseTextareaList(draft.blacklist.path_regexes),
|
||||
user_agents: parseTextareaList(draft.blacklist.user_agents),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function PowListCard({
|
||||
title,
|
||||
scope,
|
||||
draft,
|
||||
onUpdate,
|
||||
}: {
|
||||
title: string;
|
||||
scope: keyof PowListDraft;
|
||||
draft: PowListDraft;
|
||||
onUpdate: (
|
||||
scope: keyof PowListDraft,
|
||||
key: keyof ProxyRoutePoWConfig['whitelist'],
|
||||
value: string,
|
||||
) => void;
|
||||
}) {
|
||||
const fields: Array<{
|
||||
key: keyof ProxyRoutePoWConfig['whitelist'];
|
||||
label: string;
|
||||
placeholder: string;
|
||||
}> = [
|
||||
{key: 'ips', label: 'IP', placeholder: '每行一个 IP'},
|
||||
{key: 'ip_cidrs', label: 'IP CIDR', placeholder: '每行一个网段,如 10.0.0.0/8'},
|
||||
{key: 'paths', label: '路径', placeholder: '每行一个路径前缀'},
|
||||
{key: 'path_regexes', label: '路径正则', placeholder: '每行一个正则表达式'},
|
||||
{key: 'user_agents', label: 'User-Agent', placeholder: '每行一个 UA 关键词或正则'},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-lg border border-dashed p-4">
|
||||
<p className="text-sm font-medium">{title}</p>
|
||||
<div className="space-y-3">
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">{field.label}</Label>
|
||||
<Textarea
|
||||
value={draft[scope][field.key]}
|
||||
onChange={(event) => onUpdate(scope, field.key, event.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
className="min-h-16 resize-y font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PowConfigPanel({enabled, config, onChange}: PowConfigPanelProps) {
|
||||
const [draft, setDraft] = useState(() => buildDraft(config));
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(buildDraft(config));
|
||||
}, [config]);
|
||||
|
||||
const emitChange = (
|
||||
nextEnabled: boolean,
|
||||
nextConfig: Partial<ProxyRoutePoWConfig>,
|
||||
nextDraft?: PowListDraft,
|
||||
) => {
|
||||
const merged = {
|
||||
...config,
|
||||
...nextConfig,
|
||||
};
|
||||
onChange(nextEnabled, nextDraft ? applyDraft(merged, nextDraft) : merged);
|
||||
};
|
||||
|
||||
const updateList = (
|
||||
scope: keyof PowListDraft,
|
||||
key: keyof ProxyRoutePoWConfig['whitelist'],
|
||||
value: string,
|
||||
) => {
|
||||
const nextDraft: PowListDraft = {
|
||||
...draft,
|
||||
[scope]: {
|
||||
...draft[scope],
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
setDraft(nextDraft);
|
||||
emitChange(enabled, {}, nextDraft);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between rounded-lg border border-dashed p-4">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 pr-4">
|
||||
<Label htmlFor="pow-enabled">启用 PoW 防护</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
算法 {config.algorithm} · 难度 {config.difficulty}
|
||||
启用后,命中该规则组的请求需要完成浏览器计算挑战。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="pow-enabled"
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) => onChange(checked, config)}
|
||||
onCheckedChange={(checked) => emitChange(checked, {})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">算法</Label>
|
||||
<Select
|
||||
value={config.algorithm}
|
||||
onValueChange={(value: 'fast' | 'slow') =>
|
||||
emitChange(enabled, {algorithm: value})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fast">Fast</SelectItem>
|
||||
<SelectItem value="slow">Slow</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">难度</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={16}
|
||||
value={config.difficulty}
|
||||
onChange={(event) =>
|
||||
emitChange(enabled, {difficulty: Number(event.target.value) || 1})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">会话 TTL (秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={60}
|
||||
value={config.session_ttl}
|
||||
onChange={(event) =>
|
||||
emitChange(enabled, {session_ttl: Number(event.target.value) || 60})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">挑战 TTL (秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={30}
|
||||
value={config.challenge_ttl}
|
||||
onChange={(event) =>
|
||||
emitChange(enabled, {challenge_ttl: Number(event.target.value) || 30})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
<PowListCard
|
||||
title="白名单(跳过 PoW)"
|
||||
scope="whitelist"
|
||||
draft={draft}
|
||||
onUpdate={updateList}
|
||||
/>
|
||||
<PowListCard
|
||||
title="黑名单(必须 PoW)"
|
||||
scope="blacklist"
|
||||
draft={draft}
|
||||
onUpdate={updateList}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ sidebar: false
|
||||
- Wavelet 全球态势板视觉对齐系统风格:改用 `Card` / `Badge` / `Progress` 与虚线边框指标卡,地图配色与 tooltip 跟随主题 token。
|
||||
- Wavelet 全球态势板布局重构:关键指标前置为摘要栅格,地图与容量诊断并排展示,移动端自动堆叠。
|
||||
- 修复 Wavelet 全球态势板 ECharts 地图在紧凑布局下容器尺寸为 0 导致无法渲染的问题。
|
||||
- Wavelet 全球态势板地图容器调整为横向看板比例,避免世界地图被压缩成窄竖卡片。
|
||||
- Wavelet 全球态势板地图容器调整为约 2:3(宽:高)比例,避免世界地图横向过度拉伸。
|
||||
- Wavelet WAF / 代理规则认证实装完整 PoW 配置面板:支持难度、算法、TTL 与路径/IP/UA 黑白名单编辑。
|
||||
|
||||
- 将 `openflare-server` 后端业务域迁移至 `Wavelet/internal/apps/openflare/`,通过 `/api/*` legacy 兼容层保持旧前端 API 路径不变。
|
||||
- 新增 OpenFlare 业务表 goose 迁移(`of_options`、`of_origins`、`of_proxy_routes`、`of_nodes`、`of_waf_*`、`of_tls_*`、`of_config_versions`、`of_pages_*`、`of_apply_logs` 等)。
|
||||
|
||||
Reference in New Issue
Block a user