修复代理路由详情认证配置 Tab:移除 PoW 配置(PoW 仅在 WAF 规则组中设置);保留 Basic Auth 保存能力;移除页头重复的「保存当前分区」按钮。

This commit is contained in:
ryan
2026-06-21 11:26:37 +08:00
parent e479ae75e6
commit 0f904b4b6d
8 changed files with 87 additions and 171 deletions
+2
View File
@@ -22,6 +22,8 @@ sidebar: false
### 修复
- 修复代理路由详情认证配置 Tab:移除 PoW 配置(PoW 仅在 WAF 规则组中设置);保留 Basic Auth 保存能力;移除页头重复的「保存当前分区」按钮。
- 修复 Pages 路由发布失败并报 `pages module is not available`:配置快照发布流程补齐 Pages 项目激活部署解析与 `pages_deployment` 写入。
- 修复仪表盘与节点详情「24 小时网络趋势」误按速率展示:改为 OpenResty 入/出站小时流量与近 24 小时总量摘要,Y 轴与 tooltip 自动换算 B/KB/MB/GB。
@@ -312,8 +312,6 @@ export function buildPayloadFromRoute(
cache_rules: route.cache_rule_list,
custom_headers: route.custom_header_list,
remark: route.remark || '',
pow_enabled: route.pow_enabled,
pow_config: JSON.stringify(route.pow_config),
basic_auth_enabled: route.basic_auth_enabled,
basic_auth_username: route.basic_auth_username,
basic_auth_password: route.basic_auth_password,
@@ -114,8 +114,6 @@ export function ProxyRouteCreateSheet({
cache_policy: 'url',
cache_rules: [],
custom_headers: [],
pow_enabled: false,
pow_config: '{}',
basic_auth_enabled: false,
remark: values.remark.trim(),
upstream_type: 'direct',
@@ -1,31 +1,15 @@
'use client';
import {useEffect, useState} from 'react';
import {useEffect} from 'react';
import {zodResolver} from '@hookform/resolvers/zod';
import {useForm} from 'react-hook-form';
import {toast} from 'sonner';
import {z} from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@/components/ui/form';
import {Input} from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type {ProxyRouteItem, ProxyRoutePoWConfig} from '@/lib/services/openflare';
import {defaultPowConfig} from '@/app/(main)/waf/components/helpers';
import {PowConfigPanel} from '@/app/(main)/waf/components/pow-config-panel';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
import type {ProxyRouteItem} from '@/lib/services/openflare';
import {proxyRouteFormIds} from '../helpers';
import {useRouteSectionSave} from '../hooks/use-route-section-save';
@@ -33,83 +17,34 @@ import {SectionShell} from './section-shell';
const authSchema = z
.object({
auth_mode: z.enum(['none', 'basic', 'pow']),
auth_mode: z.enum(['none', 'basic']),
basic_auth_username: z.string(),
basic_auth_password: z.string(),
pow_enabled: z.boolean(),
pow_difficulty: z.string(),
pow_algorithm: z.enum(['fast', 'slow']),
pow_session_ttl: z.string(),
pow_challenge_ttl: z.string(),
})
.superRefine((value, context) => {
if (value.auth_mode === 'pow') {
const difficulty = Number(value.pow_difficulty);
if (!Number.isFinite(difficulty) || difficulty < 1 || difficulty > 16) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['pow_difficulty'],
message: '难度需为 1-16 的整数',
});
}
const sessionTtl = Number(value.pow_session_ttl);
if (!Number.isFinite(sessionTtl) || sessionTtl < 60) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['pow_session_ttl'],
message: '会话 TTL 需大于等于 60 秒',
});
}
const challengeTtl = Number(value.pow_challenge_ttl);
if (!Number.isFinite(challengeTtl) || challengeTtl < 30) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['pow_challenge_ttl'],
message: '挑战 TTL 需大于等于 30 秒',
});
}
if (value.auth_mode !== 'basic') {
return;
}
if (value.auth_mode === 'basic') {
if (!value.basic_auth_username.trim()) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['basic_auth_username'],
message: '请输入账号',
});
}
if (!value.basic_auth_password.trim()) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['basic_auth_password'],
message: '请输入密码',
});
}
if (!value.basic_auth_username.trim()) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['basic_auth_username'],
message: '请输入账号',
});
}
if (!value.basic_auth_password.trim()) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['basic_auth_password'],
message: '请输入密码',
});
}
});
type AuthValues = z.infer<typeof authSchema>;
function resolveAuthMode(route: ProxyRouteItem): AuthValues['auth_mode'] {
if (route.basic_auth_enabled) {
return 'basic';
}
if (route.pow_enabled) {
return 'pow';
}
return 'none';
}
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));
return route.basic_auth_enabled ? 'basic' : 'none';
}
interface AuthSectionProps {
@@ -120,9 +55,6 @@ interface AuthSectionProps {
export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectionProps) {
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
const [powConfig, setPowConfig] = useState<ProxyRoutePoWConfig>(
route.pow_config ?? defaultPowConfig,
);
const form = useForm<AuthValues>({
resolver: zodResolver(authSchema),
@@ -130,26 +62,14 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
auth_mode: resolveAuthMode(route),
basic_auth_username: route.basic_auth_username || '',
basic_auth_password: route.basic_auth_password || '',
pow_enabled: route.pow_enabled,
pow_difficulty: String(route.pow_config?.difficulty ?? defaultPowConfig.difficulty),
pow_algorithm: route.pow_config?.algorithm ?? defaultPowConfig.algorithm,
pow_session_ttl: String(route.pow_config?.session_ttl ?? defaultPowConfig.session_ttl),
pow_challenge_ttl: String(route.pow_config?.challenge_ttl ?? defaultPowConfig.challenge_ttl),
},
});
useEffect(() => {
const nextPowConfig = route.pow_config ?? defaultPowConfig;
setPowConfig(nextPowConfig);
form.reset({
auth_mode: resolveAuthMode(route),
basic_auth_username: route.basic_auth_username || '',
basic_auth_password: route.basic_auth_password || '',
pow_enabled: route.pow_enabled,
pow_difficulty: String(nextPowConfig.difficulty),
pow_algorithm: nextPowConfig.algorithm,
pow_session_ttl: String(nextPowConfig.session_ttl),
pow_challenge_ttl: String(nextPowConfig.challenge_ttl),
});
}, [form, route]);
@@ -158,7 +78,7 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
return (
<SectionShell
title="认证配置"
description="配置基础鉴权或 PoW 防护,限制未授权访问。"
description="配置 Basic Auth 限制未授权访问。PoW 防护请在 WAF 规则组中配置。"
formId={proxyRouteFormIds.auth}
saving={saving}
>
@@ -166,26 +86,23 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
<form
id={proxyRouteFormIds.auth}
className="space-y-5"
onSubmit={form.handleSubmit(async (values) => {
const nextPowConfig =
values.auth_mode === 'pow' ? powConfig : route.pow_config ?? defaultPowConfig;
const powEnabled = values.auth_mode === 'pow' || values.pow_enabled;
await save(
{
basic_auth_enabled: values.auth_mode === 'basic',
basic_auth_username:
values.auth_mode === 'basic' ? values.basic_auth_username.trim() : '',
basic_auth_password:
values.auth_mode === 'basic' ? values.basic_auth_password.trim() : '',
pow_enabled: powEnabled,
pow_config: JSON.stringify(
values.auth_mode === 'pow' ? nextPowConfig : route.pow_config ?? defaultPowConfig,
),
},
'认证配置已保存',
);
})}
onSubmit={form.handleSubmit(
async (values) => {
await save(
{
basic_auth_enabled: values.auth_mode === 'basic',
basic_auth_username:
values.auth_mode === 'basic' ? values.basic_auth_username.trim() : '',
basic_auth_password:
values.auth_mode === 'basic' ? values.basic_auth_password.trim() : '',
},
'认证配置已保存',
);
},
() => {
toast.error('请检查认证配置表单');
},
)}
>
<FormField
control={form.control}
@@ -193,7 +110,16 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
render={({ field }) => (
<FormItem>
<FormLabel>认证模式</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<Select
value={field.value}
onValueChange={(mode) => {
field.onChange(mode);
if (mode !== 'basic') {
form.setValue('basic_auth_username', '');
form.setValue('basic_auth_password', '');
}
}}
>
<FormControl>
<SelectTrigger>
<SelectValue />
@@ -202,10 +128,9 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
<SelectContent>
<SelectItem value="none">无认证</SelectItem>
<SelectItem value="basic">Basic Auth</SelectItem>
<SelectItem value="pow">PoW 防护</SelectItem>
</SelectContent>
</Select>
<FormDescription>同一站点仅启用一种认证模式。</FormDescription>
<FormDescription>PoW 防护仅支持在 WAF 规则组中启用并绑定站点。</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -242,20 +167,8 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
/>
</>
) : null}
{authMode === 'pow' ? (
<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>
</SectionShell>
);
}
}
@@ -2,7 +2,7 @@
import Link from 'next/link';
import {useCallback, useState} from 'react';
import {ArrowLeft, Loader2, Route, Save, Upload} from 'lucide-react';
import {ArrowLeft, Loader2, Route, Upload} from 'lucide-react';
import {toast} from 'sonner';
import {
@@ -17,11 +17,10 @@ import {
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
import {DiffDialog} from '@/app/(main)/config-versions/components/diff-dialog';
import type {ConfigDiffResult, ProxyRouteConfigSection, ProxyRouteItem} from '@/lib/services/openflare';
import type {ConfigDiffResult, ProxyRouteItem} from '@/lib/services/openflare';
import {ConfigVersionService} from '@/lib/services/openflare';
import {getErrorMessage} from '../../components/helpers';
import {submitProxyRouteSectionForm} from '../helpers';
function hasConfigDiff(diff: ConfigDiffResult) {
return (
@@ -37,11 +36,9 @@ function hasConfigDiff(diff: ConfigDiffResult) {
interface RouteHeaderProps {
route: ProxyRouteItem;
activeSection: ProxyRouteConfigSection;
saving?: boolean;
}
export function RouteHeader({ route, activeSection, saving = false }: RouteHeaderProps) {
export function RouteHeader({ route }: RouteHeaderProps) {
const [diffOpen, setDiffOpen] = useState(false);
const [publishConfirmOpen, setPublishConfirmOpen] = useState(false);
const [diff, setDiff] = useState<ConfigDiffResult | null>(null);
@@ -49,13 +46,6 @@ export function RouteHeader({ route, activeSection, saving = false }: RouteHeade
const [diffError, setDiffError] = useState<string | null>(null);
const [publishing, setPublishing] = useState(false);
const handleSave = useCallback(() => {
const submitted = submitProxyRouteSectionForm(activeSection);
if (!submitted) {
toast.error('当前分区无法保存');
}
}, [activeSection]);
const loadDiff = useCallback(async () => {
setDiffLoading(true);
setDiffError(null);
@@ -122,16 +112,6 @@ export function RouteHeader({ route, activeSection, saving = false }: RouteHeade
</div>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
variant="secondary"
className="h-8 gap-1.5 text-xs"
disabled={saving}
onClick={handleSave}
>
{saving ? <Loader2 className="size-3.5 animate-spin" /> : <Save className="size-3.5" />}
{saving ? '保存中...' : '保存当前分区'}
</Button>
<Button
size="sm"
className="h-8 gap-1.5 text-xs"
@@ -118,7 +118,7 @@ export function ProxyRouteDetailPageClient() {
return (
<div className="py-6 px-1 space-y-6">
<RouteHeader route={route} activeSection={activeSection} saving={sectionSaving} />
<RouteHeader route={route} />
<Tabs
value={activeSection}
-4
View File
@@ -255,8 +255,6 @@ export interface ProxyRouteItem {
cache_rule_list: string[];
custom_headers: string;
custom_header_list: ProxyRouteCustomHeader[];
pow_enabled: boolean;
pow_config: ProxyRoutePoWConfig;
basic_auth_enabled: boolean;
basic_auth_username: string;
basic_auth_password: string;
@@ -296,8 +294,6 @@ export interface ProxyRouteMutationPayload {
cache_policy: string;
cache_rules: string[];
custom_headers: ProxyRouteCustomHeader[];
pow_enabled: boolean;
pow_config: string;
basic_auth_enabled: boolean;
basic_auth_username?: string;
basic_auth_password?: string;
@@ -115,3 +115,32 @@ func TestValidateProxyRouteIdentityUniquenessUsesDecodedPrimaryDomain(t *testing
require.Error(t, err)
assert.Contains(t, err.Error(), "site_name already exists")
}
func TestUpdateProxyRouteAuthConfig(t *testing.T) {
cleanup := setupProxyRouteTestDB(t)
defer cleanup()
ctx := context.Background()
created, err := CreateProxyRoute(ctx, Input{
SiteName: "auth-site",
Domain: "auth.example.com",
OriginURL: "http://origin.example.com:8080",
Enabled: true,
})
require.NoError(t, err)
updated, err := UpdateProxyRoute(ctx, created.ID, Input{
SiteName: created.SiteName,
Domain: created.Domain,
Domains: created.Domains,
OriginURL: created.OriginURL,
Enabled: created.Enabled,
BasicAuthEnabled: true,
BasicAuthUsername: "admin",
BasicAuthPassword: "secret",
})
require.NoError(t, err)
assert.True(t, updated.BasicAuthEnabled)
assert.Equal(t, "admin", updated.BasicAuthUsername)
assert.Equal(t, "secret", updated.BasicAuthPassword)
}