mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
修复代理路由详情认证配置 Tab:移除 PoW 配置(PoW 仅在 WAF 规则组中设置);保留 Basic Auth 保存能力;移除页头重复的「保存当前分区」按钮。
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user