mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
prettier
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from "react"
|
||||
import {useMutation, useQuery, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseQueryResult,
|
||||
} from '@tanstack/react-query';
|
||||
import {
|
||||
Clock,
|
||||
Fingerprint,
|
||||
@@ -14,263 +19,309 @@ import {
|
||||
Settings,
|
||||
Shield,
|
||||
Trash2,
|
||||
UserPlus
|
||||
} from "lucide-react"
|
||||
UserPlus,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||
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 {AuthSourceModal} from "@/components/common/settings/auth-source-modal"
|
||||
import services from "@/lib/services"
|
||||
import type {AuthSource, SystemConfig} from "@/lib/services/admin"
|
||||
import {toast} from "sonner"
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
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 { AuthSourceModal } from '@/components/common/settings/auth-source-modal';
|
||||
import services from '@/lib/services';
|
||||
import type { AuthSource, SystemConfig } from '@/lib/services/admin';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const SECURITY_KEYS = [
|
||||
{
|
||||
key: "password_login_enabled",
|
||||
title: "允许密码登录",
|
||||
description: "关闭后仅保留第三方 OIDC 认证源进行系统登录。",
|
||||
key: 'password_login_enabled',
|
||||
title: '允许密码登录',
|
||||
description: '关闭后仅保留第三方 OIDC 认证源进行系统登录。',
|
||||
icon: Lock,
|
||||
},
|
||||
{
|
||||
key: "registration_enabled",
|
||||
title: "允许注册",
|
||||
description: "关闭后系统将禁止新用户进行自主账号注册。",
|
||||
key: 'registration_enabled',
|
||||
title: '允许注册',
|
||||
description: '关闭后系统将禁止新用户进行自主账号注册。',
|
||||
icon: UserPlus,
|
||||
},
|
||||
{
|
||||
key: "password_register_enabled",
|
||||
title: "允许密码注册",
|
||||
description: "关闭后只能通过管理员创建或第三方认证关联建号。",
|
||||
key: 'password_register_enabled',
|
||||
title: '允许密码注册',
|
||||
description: '关闭后只能通过管理员创建或第三方认证关联建号。',
|
||||
icon: Fingerprint,
|
||||
},
|
||||
{
|
||||
key: "oidc_login_enabled",
|
||||
title: "允许 OIDC 登录",
|
||||
description: "关闭后所有的第三方 OIDC 认证登录入口都会被隐藏。",
|
||||
key: 'oidc_login_enabled',
|
||||
title: '允许 OIDC 登录',
|
||||
description: '关闭后所有的第三方 OIDC 认证登录入口都会被隐藏。',
|
||||
icon: Globe,
|
||||
},
|
||||
{
|
||||
key: "email_login_verification_enabled",
|
||||
title: "邮箱登录验证",
|
||||
description: "开启后,使用账号密码登录时需要通过邮箱接收并验证 6 位验证码。",
|
||||
key: 'email_login_verification_enabled',
|
||||
title: '邮箱登录验证',
|
||||
description:
|
||||
'开启后,使用账号密码登录时需要通过邮箱接收并验证 6 位验证码。',
|
||||
icon: Mail,
|
||||
},
|
||||
{
|
||||
key: "email_register_verification_enabled",
|
||||
title: "邮箱注册验证",
|
||||
description: "开启后,用户注册账号时需要通过邮箱接收并验证 6 位验证码。",
|
||||
key: 'email_register_verification_enabled',
|
||||
title: '邮箱注册验证',
|
||||
description: '开启后,用户注册账号时需要通过邮箱接收并验证 6 位验证码。',
|
||||
icon: Mail,
|
||||
},
|
||||
] as const
|
||||
] as const;
|
||||
|
||||
interface SecurityTabProps {
|
||||
configs: Record<string, SystemConfig>
|
||||
systemConfigsQuery: UseQueryResult<SystemConfig[], Error>
|
||||
configs: Record<string, SystemConfig>;
|
||||
systemConfigsQuery: UseQueryResult<SystemConfig[], Error>;
|
||||
}
|
||||
|
||||
export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [authSourceModalOpen, setAuthSourceModalOpen] = useState(false)
|
||||
const [selectedSource, setSelectedSource] = useState<AuthSource | null>(null)
|
||||
const queryClient = useQueryClient();
|
||||
const [authSourceModalOpen, setAuthSourceModalOpen] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState<AuthSource | null>(null);
|
||||
|
||||
const [capCount, setCapCount] = useState("")
|
||||
const [capDifficulty, setCapDifficulty] = useState("")
|
||||
const [capSize, setCapSize] = useState("")
|
||||
const [capTTL, setCapTTL] = useState("")
|
||||
const [capTokenTTL, setCapTokenTTL] = useState("")
|
||||
const [capAutoSolve, setCapAutoSolve] = useState(true)
|
||||
const [capCount, setCapCount] = useState('');
|
||||
const [capDifficulty, setCapDifficulty] = useState('');
|
||||
const [capSize, setCapSize] = useState('');
|
||||
const [capTTL, setCapTTL] = useState('');
|
||||
const [capTokenTTL, setCapTokenTTL] = useState('');
|
||||
const [capAutoSolve, setCapAutoSolve] = useState(true);
|
||||
|
||||
const [sessionTTL, setSessionTTL] = useState("168")
|
||||
const [customHours, setCustomHours] = useState("")
|
||||
const [sessionTTL, setSessionTTL] = useState('168');
|
||||
const [customHours, setCustomHours] = useState('');
|
||||
|
||||
const authSourcesQuery = useQuery({
|
||||
queryKey: ["auth", "sources"],
|
||||
queryKey: ['auth', 'sources'],
|
||||
queryFn: () => services.adminAuthSource.listAuthSources(),
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (systemConfigsQuery.data) {
|
||||
const cfgMap = configs
|
||||
setCapCount(cfgMap["cap_challenge_count"]?.value || "1")
|
||||
setCapDifficulty(cfgMap["cap_challenge_difficulty"]?.value || "4")
|
||||
setCapSize(cfgMap["cap_challenge_size"]?.value || "32")
|
||||
setCapTTL(cfgMap["cap_challenge_ttl_seconds"]?.value || "600")
|
||||
setCapTokenTTL(cfgMap["cap_token_ttl_seconds"]?.value || "1200")
|
||||
setCapAutoSolve(cfgMap["cap_auto_solve"]?.value !== "false")
|
||||
const cfgMap = configs;
|
||||
setCapCount(cfgMap['cap_challenge_count']?.value || '1');
|
||||
setCapDifficulty(cfgMap['cap_challenge_difficulty']?.value || '4');
|
||||
setCapSize(cfgMap['cap_challenge_size']?.value || '32');
|
||||
setCapTTL(cfgMap['cap_challenge_ttl_seconds']?.value || '600');
|
||||
setCapTokenTTL(cfgMap['cap_token_ttl_seconds']?.value || '1200');
|
||||
setCapAutoSolve(cfgMap['cap_auto_solve']?.value !== 'false');
|
||||
|
||||
// 初始化登录保持设置
|
||||
const ttlVal = cfgMap["login_session_ttl_hours"]?.value || "0"
|
||||
if (ttlVal === "0" || ttlVal === "168" || ttlVal === "720" || ttlVal === "-1") {
|
||||
setSessionTTL(ttlVal)
|
||||
setCustomHours("")
|
||||
const ttlVal = cfgMap['login_session_ttl_hours']?.value || '0';
|
||||
if (
|
||||
ttlVal === '0' ||
|
||||
ttlVal === '168' ||
|
||||
ttlVal === '720' ||
|
||||
ttlVal === '-1'
|
||||
) {
|
||||
setSessionTTL(ttlVal);
|
||||
setCustomHours('');
|
||||
} else {
|
||||
setSessionTTL("custom")
|
||||
setCustomHours(ttlVal)
|
||||
setSessionTTL('custom');
|
||||
setCustomHours(ttlVal);
|
||||
}
|
||||
}
|
||||
}, [systemConfigsQuery.data, configs])
|
||||
}, [systemConfigsQuery.data, configs]);
|
||||
|
||||
const updateTTLMutation = useMutation({
|
||||
mutationFn: async (value: string) => {
|
||||
const config = configs["login_session_ttl_hours"]
|
||||
const config = configs['login_session_ttl_hours'];
|
||||
if (!config) {
|
||||
throw new Error("缺少配置项: login_session_ttl_hours")
|
||||
throw new Error('缺少配置项: login_session_ttl_hours');
|
||||
}
|
||||
await services.adminSystemConfig.updateSystemConfig("login_session_ttl_hours", {
|
||||
value: value,
|
||||
description: config.description,
|
||||
})
|
||||
await services.adminSystemConfig.updateSystemConfig(
|
||||
'login_session_ttl_hours',
|
||||
{
|
||||
value: value,
|
||||
description: config.description,
|
||||
},
|
||||
);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
toast.success("登录状态保持时间已更新")
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin', 'system-configs'],
|
||||
});
|
||||
toast.success('登录状态保持时间已更新');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "更新配置失败")
|
||||
toast.error(error.message || '更新配置失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleTTLChange = (val: string) => {
|
||||
setSessionTTL(val)
|
||||
if (val !== "custom") {
|
||||
updateTTLMutation.mutate(val)
|
||||
setSessionTTL(val);
|
||||
if (val !== 'custom') {
|
||||
updateTTLMutation.mutate(val);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomBlur = () => {
|
||||
const parsed = parseInt(customHours, 10)
|
||||
const parsed = parseInt(customHours, 10);
|
||||
if (isNaN(parsed) || parsed <= 0) {
|
||||
toast.error("请输入有效的过期小时数(正整数)")
|
||||
toast.error('请输入有效的过期小时数(正整数)');
|
||||
// 重置为原本的值
|
||||
const originalVal = configs["login_session_ttl_hours"]?.value || "0"
|
||||
setCustomHours(originalVal === "custom" || ["0", "168", "720", "-1"].includes(originalVal) ? "" : originalVal)
|
||||
return
|
||||
const originalVal = configs['login_session_ttl_hours']?.value || '0';
|
||||
setCustomHours(
|
||||
originalVal === 'custom' ||
|
||||
['0', '168', '720', '-1'].includes(originalVal)
|
||||
? ''
|
||||
: originalVal,
|
||||
);
|
||||
return;
|
||||
}
|
||||
updateTTLMutation.mutate(parsed.toString())
|
||||
}
|
||||
updateTTLMutation.mutate(parsed.toString());
|
||||
};
|
||||
|
||||
const updateConfigMutation = useMutation({
|
||||
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
|
||||
const config = configs[key]
|
||||
const config = configs[key];
|
||||
if (!config) {
|
||||
throw new Error(`缺少配置项: ${key}`)
|
||||
throw new Error(`缺少配置项: ${key}`);
|
||||
}
|
||||
await services.adminSystemConfig.updateSystemConfig(key, {
|
||||
value: value ? "true" : "false",
|
||||
value: value ? 'true' : 'false',
|
||||
description: config.description,
|
||||
})
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["public-config"] })
|
||||
toast.success("系统安全配置已更新")
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin', 'system-configs'],
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ['public-config'] });
|
||||
toast.success('系统安全配置已更新');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "更新配置失败")
|
||||
toast.error(error.message || '更新配置失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const toggleSourceMutation = useMutation({
|
||||
mutationFn: async (source: AuthSource) => {
|
||||
await services.adminAuthSource.toggleAuthSource(source.id, { is_active: !source.is_active })
|
||||
await services.adminAuthSource.toggleAuthSource(source.id, {
|
||||
is_active: !source.is_active,
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth", "sources"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth", "public-sources"] })
|
||||
toast.success("认证源状态已更新")
|
||||
await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'] });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['auth', 'public-sources'],
|
||||
});
|
||||
toast.success('认证源状态已更新');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "切换状态失败")
|
||||
toast.error(error.message || '切换状态失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const deleteSourceMutation = useMutation({
|
||||
mutationFn: async (sourceId: string) => {
|
||||
await services.adminAuthSource.deleteAuthSource(sourceId)
|
||||
await services.adminAuthSource.deleteAuthSource(sourceId);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth", "sources"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth", "public-sources"] })
|
||||
toast.success("认证源已删除")
|
||||
await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'] });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['auth', 'public-sources'],
|
||||
});
|
||||
toast.success('认证源已删除');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "删除认证源失败")
|
||||
toast.error(error.message || '删除认证源失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleToggle = (key: string, checked: boolean) => {
|
||||
updateConfigMutation.mutate({ key, value: checked })
|
||||
}
|
||||
updateConfigMutation.mutate({ key, value: checked });
|
||||
};
|
||||
|
||||
const saveCapMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const updates = [
|
||||
{ key: "cap_challenge_count", value: capCount },
|
||||
{ key: "cap_challenge_difficulty", value: capDifficulty },
|
||||
{ key: "cap_challenge_size", value: capSize },
|
||||
{ key: "cap_challenge_ttl_seconds", value: capTTL },
|
||||
{ key: "cap_token_ttl_seconds", value: capTokenTTL },
|
||||
{ key: "cap_auto_solve", value: capAutoSolve ? "true" : "false" },
|
||||
]
|
||||
{ key: 'cap_challenge_count', value: capCount },
|
||||
{ key: 'cap_challenge_difficulty', value: capDifficulty },
|
||||
{ key: 'cap_challenge_size', value: capSize },
|
||||
{ key: 'cap_challenge_ttl_seconds', value: capTTL },
|
||||
{ key: 'cap_token_ttl_seconds', value: capTokenTTL },
|
||||
{ key: 'cap_auto_solve', value: capAutoSolve ? 'true' : 'false' },
|
||||
];
|
||||
|
||||
for (const update of updates) {
|
||||
const currentCfg = configs[update.key]
|
||||
const currentCfg = configs[update.key];
|
||||
await services.adminSystemConfig.updateSystemConfig(update.key, {
|
||||
value: update.value,
|
||||
description: currentCfg?.description || "",
|
||||
})
|
||||
description: currentCfg?.description || '',
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
toast.success("人机验证配置已成功保存")
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin', 'system-configs'],
|
||||
});
|
||||
toast.success('人机验证配置已成功保存');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "保存配置失败")
|
||||
toast.error(error.message || '保存配置失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleCapSave = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
saveCapMutation.mutate()
|
||||
}
|
||||
e.preventDefault();
|
||||
saveCapMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className='space-y-6'>
|
||||
{/* 系统登录与注册控制 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-primary/10 text-primary">
|
||||
<Settings className="size-4" />
|
||||
<Card className='border border-dashed shadow-sm'>
|
||||
<CardHeader className='border-b border-dashed pb-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='p-1.5 rounded-lg bg-primary/10 text-primary'>
|
||||
<Settings className='size-4' />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">系统登录/注册设置</CardTitle>
|
||||
<CardDescription className="text-xs">配置系统的登录限制与用户自主注册权限</CardDescription>
|
||||
<CardTitle className='text-base font-semibold'>
|
||||
系统登录/注册设置
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
配置系统的登录限制与用户自主注册权限
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<CardContent className='pt-6'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
{SECURITY_KEYS.map((item) => {
|
||||
const config = configs[item.key]
|
||||
const checked = config ? config.value === "true" : false
|
||||
const Icon = item.icon
|
||||
const config = configs[item.key];
|
||||
const checked = config ? config.value === 'true' : false;
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className="flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-primary/30 transition-all duration-300 shadow-sm"
|
||||
className='flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-primary/30 transition-all duration-300 shadow-sm'
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{Icon && <Icon className="size-4 text-primary" />}
|
||||
<span className="font-medium text-sm text-foreground">{item.title}</span>
|
||||
<div className='space-y-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{Icon && <Icon className='size-4 text-primary' />}
|
||||
<span className='font-medium text-sm text-foreground'>
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed pr-2">{item.description}</p>
|
||||
<p className='text-xs text-muted-foreground leading-relaxed pr-2'>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={checked}
|
||||
@@ -278,56 +329,56 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
onCheckedChange={(value) => handleToggle(item.key, value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 登录状态保持时间 (选择后立即更改) */}
|
||||
<div
|
||||
className="flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-primary/30 transition-all duration-300 shadow-sm md:col-span-2"
|
||||
>
|
||||
<div className="space-y-1 pr-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="size-4 text-primary" />
|
||||
<span className="font-medium text-sm text-foreground">登录状态保持时间</span>
|
||||
<div className='flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-primary/30 transition-all duration-300 shadow-sm md:col-span-2'>
|
||||
<div className='space-y-1 pr-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Clock className='size-4 text-primary' />
|
||||
<span className='font-medium text-sm text-foreground'>
|
||||
登录状态保持时间
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed pr-2">
|
||||
<p className='text-xs text-muted-foreground leading-relaxed pr-2'>
|
||||
配置用户登录会话在浏览器中的保持期限。设置为“关闭”则在浏览器关闭后自动退登。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className='flex items-center gap-2 flex-shrink-0'>
|
||||
<Select
|
||||
value={sessionTTL}
|
||||
disabled={updateTTLMutation.isPending}
|
||||
onValueChange={handleTTLChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] bg-card border-dashed text-xs h-8">
|
||||
<SelectValue placeholder="选择保留时间" />
|
||||
<SelectTrigger className='w-[180px] bg-card border-dashed text-xs h-8'>
|
||||
<SelectValue placeholder='选择保留时间' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">关闭 (浏览器关闭自动退登)</SelectItem>
|
||||
<SelectItem value="168">7 天</SelectItem>
|
||||
<SelectItem value="720">30 天</SelectItem>
|
||||
<SelectItem value="-1">永不过期</SelectItem>
|
||||
<SelectItem value="custom">自定义时长</SelectItem>
|
||||
<SelectItem value='0'>关闭 (浏览器关闭自动退登)</SelectItem>
|
||||
<SelectItem value='168'>7 天</SelectItem>
|
||||
<SelectItem value='720'>30 天</SelectItem>
|
||||
<SelectItem value='-1'>永不过期</SelectItem>
|
||||
<SelectItem value='custom'>自定义时长</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{sessionTTL === "custom" && (
|
||||
{sessionTTL === 'custom' && (
|
||||
<Input
|
||||
type="number"
|
||||
type='number'
|
||||
min={1}
|
||||
value={customHours}
|
||||
onChange={(e) => setCustomHours(e.target.value)}
|
||||
onBlur={handleCustomBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleCustomBlur()
|
||||
if (e.key === 'Enter') {
|
||||
handleCustomBlur();
|
||||
}
|
||||
}}
|
||||
placeholder="小时"
|
||||
placeholder='小时'
|
||||
disabled={updateTTLMutation.isPending}
|
||||
className="w-20 bg-card border-dashed text-xs h-8 px-2"
|
||||
className='w-20 bg-card border-dashed text-xs h-8 px-2'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -337,116 +388,134 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
</Card>
|
||||
|
||||
{/* 认证源配置管理 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4 flex flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-primary/10 text-primary">
|
||||
<Globe className="size-4" />
|
||||
<Card className='border border-dashed shadow-sm'>
|
||||
<CardHeader className='border-b border-dashed pb-4 flex flex-row items-center justify-between gap-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='p-1.5 rounded-lg bg-primary/10 text-primary'>
|
||||
<Globe className='size-4' />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">认证源管理</CardTitle>
|
||||
<CardDescription className="text-xs">添加、修改并启用系统自定义的 OIDC 认证源</CardDescription>
|
||||
<CardTitle className='text-base font-semibold'>
|
||||
认证源管理
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
添加、修改并启用系统自定义的 OIDC 认证源
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
type='button'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setSelectedSource(null)
|
||||
setAuthSourceModalOpen(true)
|
||||
setSelectedSource(null);
|
||||
setAuthSourceModalOpen(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
variant='secondary'
|
||||
>
|
||||
<Plus className="mr-1.5 size-3.5" />
|
||||
<Plus className='mr-1.5 size-3.5' />
|
||||
新增认证源
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6 space-y-3">
|
||||
<CardContent className='pt-6 space-y-3'>
|
||||
{authSourcesQuery.isPending ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="size-6 animate-spin text-muted-foreground/50" />
|
||||
<div className='flex items-center justify-center p-8'>
|
||||
<Loader2 className='size-6 animate-spin text-muted-foreground/50' />
|
||||
</div>
|
||||
) : (authSourcesQuery.data ?? []).length > 0 ? (
|
||||
(authSourcesQuery.data ?? []).map((source) => (
|
||||
<div
|
||||
key={source.id}
|
||||
className="flex items-center justify-between rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 transition-all duration-300 shadow-sm"
|
||||
className='flex items-center justify-between rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 transition-all duration-300 shadow-sm'
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm text-foreground">{source.display_name || source.name}</span>
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded-full border font-medium ${
|
||||
source.is_active
|
||||
? "bg-emerald-500/10 text-emerald-500 border-emerald-500/20"
|
||||
: "bg-amber-500/10 text-amber-500 border-amber-500/20"
|
||||
}`}>
|
||||
{source.is_active ? "已启用" : "已禁用"}
|
||||
<div className='space-y-1.5'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-semibold text-sm text-foreground'>
|
||||
{source.display_name || source.name}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] px-2 py-0.5 rounded-full border font-medium ${
|
||||
source.is_active
|
||||
? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20'
|
||||
: 'bg-amber-500/10 text-amber-500 border-amber-500/20'
|
||||
}`}
|
||||
>
|
||||
{source.is_active ? '已启用' : '已禁用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
<div className='text-xs text-muted-foreground font-mono'>
|
||||
标识符: {source.name} · 类型: {source.type.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={`text-xs px-2.5 py-1 rounded-lg border font-medium hidden sm:inline-block ${
|
||||
source.client_secret_configured
|
||||
? "bg-primary/5 text-primary border-primary/10"
|
||||
: "bg-rose-500/5 text-rose-500 border-rose-500/10"
|
||||
}`}>
|
||||
{source.client_secret_configured ? "Secret 已配置" : "Secret 未配置"}
|
||||
<div className='flex items-center gap-4'>
|
||||
<span
|
||||
className={`text-xs px-2.5 py-1 rounded-lg border font-medium hidden sm:inline-block ${
|
||||
source.client_secret_configured
|
||||
? 'bg-primary/5 text-primary border-primary/10'
|
||||
: 'bg-rose-500/5 text-rose-500 border-rose-500/10'
|
||||
}`}
|
||||
>
|
||||
{source.client_secret_configured
|
||||
? 'Secret 已配置'
|
||||
: 'Secret 未配置'}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
<Switch
|
||||
checked={source.is_active}
|
||||
disabled={toggleSourceMutation.isPending}
|
||||
className="scale-90 mr-2"
|
||||
onCheckedChange={() => toggleSourceMutation.mutate(source)}
|
||||
className='scale-90 mr-2'
|
||||
onCheckedChange={() =>
|
||||
toggleSourceMutation.mutate(source)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='size-8 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-lg transition-colors'
|
||||
onClick={() => {
|
||||
setSelectedSource(source)
|
||||
setAuthSourceModalOpen(true)
|
||||
setSelectedSource(source);
|
||||
setAuthSourceModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
<Pencil className='size-4' />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 text-muted-foreground hover:text-rose-500 hover:bg-rose-500/10 rounded-lg transition-colors"
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='size-8 text-muted-foreground hover:text-rose-500 hover:bg-rose-500/10 rounded-lg transition-colors'
|
||||
disabled={deleteSourceMutation.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(`确定删除认证源「${source.display_name || source.name}」吗?`)) {
|
||||
deleteSourceMutation.mutate(source.id)
|
||||
if (
|
||||
window.confirm(
|
||||
`确定删除认证源「${source.display_name || source.name}」吗?`,
|
||||
)
|
||||
) {
|
||||
deleteSourceMutation.mutate(source.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<Trash2 className='size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-border/50 px-4 py-8 text-center text-xs text-muted-foreground bg-muted/5 flex flex-col items-center justify-center gap-3">
|
||||
<div className='rounded-xl border border-dashed border-border/50 px-4 py-8 text-center text-xs text-muted-foreground bg-muted/5 flex flex-col items-center justify-center gap-3'>
|
||||
<span>暂无配置的认证源,点击上方按钮新增</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
type='button'
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => {
|
||||
setSelectedSource(null)
|
||||
setAuthSourceModalOpen(true)
|
||||
setSelectedSource(null);
|
||||
setAuthSourceModalOpen(true);
|
||||
}}
|
||||
className="border-dashed"
|
||||
className='border-dashed'
|
||||
>
|
||||
<Plus className="mr-1.5 size-3.5" />
|
||||
<Plus className='mr-1.5 size-3.5' />
|
||||
新增认证源
|
||||
</Button>
|
||||
</div>
|
||||
@@ -455,123 +524,163 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
</Card>
|
||||
|
||||
{/* 人机验证配置 (Cap CAPTCHA) */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4 flex flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-primary/10 text-primary">
|
||||
<Shield className="size-4" />
|
||||
<Card className='border border-dashed shadow-sm'>
|
||||
<CardHeader className='border-b border-dashed pb-4 flex flex-row items-center justify-between gap-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='p-1.5 rounded-lg bg-primary/10 text-primary'>
|
||||
<Shield className='size-4' />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">人机验证配置 (Cap CAPTCHA)</CardTitle>
|
||||
<CardDescription className="text-xs">配置基于 Proof-of-Work (PoW) 的无感人机验证,保护系统登录免受暴力破解和撞库攻击</CardDescription>
|
||||
<CardTitle className='text-base font-semibold'>
|
||||
人机验证配置 (Cap CAPTCHA)
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
配置基于 Proof-of-Work (PoW)
|
||||
的无感人机验证,保护系统登录免受暴力破解和撞库攻击
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={configs["cap_login_enabled"]?.value === "true"}
|
||||
checked={configs['cap_login_enabled']?.value === 'true'}
|
||||
disabled={updateConfigMutation.isPending}
|
||||
onCheckedChange={(checked) => handleToggle("cap_login_enabled", checked)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleToggle('cap_login_enabled', checked)
|
||||
}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<CardContent className='pt-6'>
|
||||
{/* 自动开始计算 Switch */}
|
||||
<div className="flex items-center justify-between rounded-xl border border-dashed p-4 bg-card mb-4">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-semibold">打开页面后自动开始计算</p>
|
||||
<div className='flex items-center justify-between rounded-xl border border-dashed p-4 bg-card mb-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<p className='text-sm font-semibold'>打开页面后自动开始计算</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={capAutoSolve}
|
||||
onCheckedChange={setCapAutoSolve}
|
||||
/>
|
||||
<Switch checked={capAutoSolve} onCheckedChange={setCapAutoSolve} />
|
||||
</div>
|
||||
<form onSubmit={handleCapSave} className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cap_challenge_count" className="text-xs font-semibold">难题数量 (Count)</Label>
|
||||
<form onSubmit={handleCapSave} className='space-y-6'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label
|
||||
htmlFor='cap_challenge_count'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
难题数量 (Count)
|
||||
</Label>
|
||||
<Input
|
||||
id="cap_challenge_count"
|
||||
type="number"
|
||||
id='cap_challenge_count'
|
||||
type='number'
|
||||
min={1}
|
||||
max={100}
|
||||
value={capCount}
|
||||
onChange={(e) => setCapCount(e.target.value)}
|
||||
placeholder="50"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='50'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">客户端需求解的难题总数。默认 1,推荐 1 至 5</p>
|
||||
<p className='text-[10px] text-muted-foreground leading-normal'>
|
||||
客户端需求解的难题总数。默认 1,推荐 1 至 5
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cap_challenge_difficulty" className="text-xs font-semibold">验证难度 (Difficulty)</Label>
|
||||
<div className='space-y-1.5'>
|
||||
<Label
|
||||
htmlFor='cap_challenge_difficulty'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
验证难度 (Difficulty)
|
||||
</Label>
|
||||
<Input
|
||||
id="cap_challenge_difficulty"
|
||||
type="number"
|
||||
id='cap_challenge_difficulty'
|
||||
type='number'
|
||||
min={1}
|
||||
max={10}
|
||||
value={capDifficulty}
|
||||
onChange={(e) => setCapDifficulty(e.target.value)}
|
||||
placeholder="4"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='4'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">PoW 前缀哈希位数,每加 1 计算时间翻倍。默认 4,推荐 4</p>
|
||||
<p className='text-[10px] text-muted-foreground leading-normal'>
|
||||
PoW 前缀哈希位数,每加 1 计算时间翻倍。默认 4,推荐 4
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cap_challenge_size" className="text-xs font-semibold">盐值长度 (Size)</Label>
|
||||
<div className='space-y-1.5'>
|
||||
<Label
|
||||
htmlFor='cap_challenge_size'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
盐值长度 (Size)
|
||||
</Label>
|
||||
<Input
|
||||
id="cap_challenge_size"
|
||||
type="number"
|
||||
id='cap_challenge_size'
|
||||
type='number'
|
||||
min={8}
|
||||
max={64}
|
||||
value={capSize}
|
||||
onChange={(e) => setCapSize(e.target.value)}
|
||||
placeholder="32"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='32'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">难题盐值混淆字符长度。默认 32</p>
|
||||
<p className='text-[10px] text-muted-foreground leading-normal'>
|
||||
难题盐值混淆字符长度。默认 32
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cap_challenge_ttl" className="text-xs font-semibold">难题超时时长 (秒)</Label>
|
||||
<div className='space-y-1.5'>
|
||||
<Label
|
||||
htmlFor='cap_challenge_ttl'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
难题超时时长 (秒)
|
||||
</Label>
|
||||
<Input
|
||||
id="cap_challenge_ttl"
|
||||
type="number"
|
||||
id='cap_challenge_ttl'
|
||||
type='number'
|
||||
min={10}
|
||||
value={capTTL}
|
||||
onChange={(e) => setCapTTL(e.target.value)}
|
||||
placeholder="600"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='600'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">难题有效期限。默认 600 秒 (10 分钟)</p>
|
||||
<p className='text-[10px] text-muted-foreground leading-normal'>
|
||||
难题有效期限。默认 600 秒 (10 分钟)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 sm:col-span-2">
|
||||
<Label htmlFor="cap_token_ttl" className="text-xs font-semibold">验证凭证有效时长 (秒)</Label>
|
||||
<div className='space-y-1.5 sm:col-span-2'>
|
||||
<Label
|
||||
htmlFor='cap_token_ttl'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
验证凭证有效时长 (秒)
|
||||
</Label>
|
||||
<Input
|
||||
id="cap_token_ttl"
|
||||
type="number"
|
||||
id='cap_token_ttl'
|
||||
type='number'
|
||||
min={10}
|
||||
value={capTokenTTL}
|
||||
onChange={(e) => setCapTokenTTL(e.target.value)}
|
||||
placeholder="1200"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='1200'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">PoW 计算求解通过后,签发的登录凭证有效时长。默认 1200 秒 (20 分钟)</p>
|
||||
<p className='text-[10px] text-muted-foreground leading-normal'>
|
||||
PoW 计算求解通过后,签发的登录凭证有效时长。默认 1200 秒 (20
|
||||
分钟)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-dashed">
|
||||
<div className='flex justify-end pt-4 border-t border-dashed'>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
type='submit'
|
||||
size='sm'
|
||||
disabled={saveCapMutation.isPending}
|
||||
>
|
||||
{saveCapMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
|
||||
<Loader2 className='mr-1.5 size-3.5 animate-spin' />
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
"保存配置"
|
||||
'保存配置'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -584,11 +693,15 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
source={selectedSource}
|
||||
onClose={() => setAuthSourceModalOpen(false)}
|
||||
onChanged={async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth", "sources"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["auth", "public-sources"] })
|
||||
await authSourcesQuery.refetch()
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['auth', 'sources'],
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['auth', 'public-sources'],
|
||||
});
|
||||
await authSourcesQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user