'use client'; import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient, type UseQueryResult, } from '@tanstack/react-query'; import { Clock, Fingerprint, Globe, Loader2, Lock, Mail, Pencil, Plus, Settings, Shield, Trash2, 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'; const SECURITY_KEYS = [ { key: 'password_login_enabled', title: '允许密码登录', description: '关闭后仅保留第三方 OIDC 认证源进行系统登录。', icon: Lock, }, { key: 'registration_enabled', title: '允许注册', description: '关闭后系统将禁止新用户进行自主账号注册。', icon: UserPlus, }, { key: 'password_register_enabled', title: '允许密码注册', description: '关闭后只能通过管理员创建或第三方认证关联建号。', icon: Fingerprint, }, { key: 'oidc_login_enabled', title: '允许 OIDC 登录', description: '关闭后所有的第三方 OIDC 认证登录入口都会被隐藏。', icon: Globe, }, { key: 'email_login_verification_enabled', title: '邮箱登录验证', description: '开启后,使用账号密码登录时需要通过邮箱接收并验证 6 位验证码。', icon: Mail, }, { key: 'email_register_verification_enabled', title: '邮箱注册验证', description: '开启后,用户注册账号时需要通过邮箱接收并验证 6 位验证码。', icon: Mail, }, ] as const; interface SecurityTabProps { configs: Record; systemConfigsQuery: UseQueryResult; } export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { const queryClient = useQueryClient(); const [authSourceModalOpen, setAuthSourceModalOpen] = useState(false); const [selectedSource, setSelectedSource] = useState(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 [sessionTTL, setSessionTTL] = useState('168'); const [customHours, setCustomHours] = useState(''); const authSourcesQuery = useQuery({ 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 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); } } }, [systemConfigsQuery.data, configs]); const updateTTLMutation = useMutation({ mutationFn: async (value: string) => { const config = configs['login_session_ttl_hours']; if (!config) { throw new Error('缺少配置项: login_session_ttl_hours'); } await services.adminSystemConfig.updateSystemConfig( 'login_session_ttl_hours', { value: value, description: config.description, }, ); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['admin', 'system-configs'], }); toast.success('登录状态保持时间已更新'); }, onError: (error: Error) => { toast.error(error.message || '更新配置失败'); }, }); const handleTTLChange = (val: string) => { setSessionTTL(val); if (val !== 'custom') { updateTTLMutation.mutate(val); } }; const handleCustomBlur = () => { const parsed = parseInt(customHours, 10); if (isNaN(parsed) || parsed <= 0) { toast.error('请输入有效的过期小时数(正整数)'); // 重置为原本的值 const originalVal = configs['login_session_ttl_hours']?.value || '0'; setCustomHours( originalVal === 'custom' || ['0', '168', '720', '-1'].includes(originalVal) ? '' : originalVal, ); return; } updateTTLMutation.mutate(parsed.toString()); }; const updateConfigMutation = useMutation({ mutationFn: async ({ key, value }: { key: string; value: boolean }) => { const config = configs[key]; if (!config) { throw new Error(`缺少配置项: ${key}`); } await services.adminSystemConfig.updateSystemConfig(key, { value: value ? 'true' : 'false', description: config.description, }); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['admin', 'system-configs'], }); await queryClient.invalidateQueries({ queryKey: ['public-config'] }); toast.success('系统安全配置已更新'); }, onError: (error: Error) => { toast.error(error.message || '更新配置失败'); }, }); const toggleSourceMutation = useMutation({ mutationFn: async (source: AuthSource) => { 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('认证源状态已更新'); }, onError: (error: Error) => { toast.error(error.message || '切换状态失败'); }, }); const deleteSourceMutation = useMutation({ mutationFn: async (sourceId: string) => { await services.adminAuthSource.deleteAuthSource(sourceId); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'] }); await queryClient.invalidateQueries({ queryKey: ['auth', 'public-sources'], }); toast.success('认证源已删除'); }, onError: (error: Error) => { toast.error(error.message || '删除认证源失败'); }, }); const handleToggle = (key: string, checked: boolean) => { 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' }, ]; for (const update of updates) { const currentCfg = configs[update.key]; await services.adminSystemConfig.updateSystemConfig(update.key, { value: update.value, description: currentCfg?.description || '', }); } }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['admin', 'system-configs'], }); toast.success('人机验证配置已成功保存'); }, onError: (error: Error) => { toast.error(error.message || '保存配置失败'); }, }); const handleCapSave = (e: React.FormEvent) => { e.preventDefault(); saveCapMutation.mutate(); }; return (
{/* 系统登录与注册控制 */}
系统登录/注册设置 配置系统的登录限制与用户自主注册权限
{SECURITY_KEYS.map((item) => { const config = configs[item.key]; const checked = config ? config.value === 'true' : false; const Icon = item.icon; return (
{Icon && } {item.title}

{item.description}

handleToggle(item.key, value)} />
); })} {/* 登录状态保持时间 (选择后立即更改) */}
登录状态保持时间

配置用户登录会话在浏览器中的保持期限。设置为“关闭”则在浏览器关闭后自动退登。

{sessionTTL === 'custom' && ( setCustomHours(e.target.value)} onBlur={handleCustomBlur} onKeyDown={(e) => { if (e.key === 'Enter') { handleCustomBlur(); } }} placeholder='小时' disabled={updateTTLMutation.isPending} className='w-20 bg-card border-dashed text-xs h-8 px-2' /> )}
{/* 认证源配置管理 */}
认证源管理 添加、修改并启用系统自定义的 OIDC 认证源
{authSourcesQuery.isPending ? (
) : (authSourcesQuery.data ?? []).length > 0 ? ( (authSourcesQuery.data ?? []).map((source) => (
{source.display_name || source.name} {source.is_active ? '已启用' : '已禁用'}
标识符: {source.name} · 类型: {source.type.toUpperCase()}
toggleSourceMutation.mutate(source) } />
)) ) : (
暂无配置的认证源,点击上方按钮新增
)}
{/* 人机验证配置 (Cap CAPTCHA) */}
人机验证配置 (Cap CAPTCHA) 配置基于 Proof-of-Work (PoW) 的无感人机验证,保护系统登录免受暴力破解和撞库攻击
handleToggle('cap_login_enabled', checked) } />
{/* 自动开始计算 Switch */}

打开页面后自动开始计算

setCapCount(e.target.value)} placeholder='50' className='bg-card border-dashed text-xs' />

客户端需求解的难题总数。默认 1,推荐 1 至 5

setCapDifficulty(e.target.value)} placeholder='4' className='bg-card border-dashed text-xs' />

PoW 前缀哈希位数,每加 1 计算时间翻倍。默认 4,推荐 4

setCapSize(e.target.value)} placeholder='32' className='bg-card border-dashed text-xs' />

难题盐值混淆字符长度。默认 32

setCapTTL(e.target.value)} placeholder='600' className='bg-card border-dashed text-xs' />

难题有效期限。默认 600 秒 (10 分钟)

setCapTokenTTL(e.target.value)} placeholder='1200' className='bg-card border-dashed text-xs' />

PoW 计算求解通过后,签发的登录凭证有效时长。默认 1200 秒 (20 分钟)

setAuthSourceModalOpen(false)} onChanged={async () => { await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'], }); await queryClient.invalidateQueries({ queryKey: ['auth', 'public-sources'], }); await authSourcesQuery.refetch(); }} />
); }