'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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; 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 { useTranslations } from 'next-intl'; const SECURITY_KEYS = [ { key: 'password_login_enabled', titleKey: 'passwordLoginEnabled', descKey: 'passwordLoginEnabledDesc', icon: Lock, }, { key: 'registration_enabled', titleKey: 'registrationEnabled', descKey: 'registrationEnabledDesc', icon: UserPlus, }, { key: 'password_register_enabled', titleKey: 'passwordRegisterEnabled', descKey: 'passwordRegisterEnabledDesc', icon: Fingerprint, }, { key: 'oidc_login_enabled', titleKey: 'oidcLoginEnabled', descKey: 'oidcLoginEnabledDesc', icon: Globe, }, { key: 'email_login_verification_enabled', titleKey: 'emailLoginVerification', descKey: 'emailLoginVerificationDesc', icon: Mail, }, { key: 'email_register_verification_enabled', titleKey: 'emailRegisterVerification', descKey: 'emailRegisterVerificationDesc', icon: Mail, }, ] as const; interface SecurityTabProps { configs: Record; systemConfigsQuery: UseQueryResult; } export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { const queryClient = useQueryClient(); const t = useTranslations('settings.security'); const tCommon = useTranslations('common'); const [authSourceModalOpen, setAuthSourceModalOpen] = useState(false); const [selectedSource, setSelectedSource] = useState(null); const [deleteTarget, setDeleteTarget] = 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(t('loginSessionTTLUpdated')); }, onError: (error: Error) => { toast.error(error.message || t('updateConfigFailed')); }, }); 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(t('invalidHours')); // 重置为原本的值 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(t('securityConfigUpdated')); }, onError: (error: Error) => { toast.error(error.message || t('updateConfigFailed')); }, }); 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(t('authSourceStatusUpdated')); }, onError: (error: Error) => { toast.error(error.message || t('toggleStatusFailed')); }, }); const deleteSourceMutation = useMutation({ mutationFn: async (sourceId: string) => { await services.adminAuthSource.deleteAuthSource(sourceId); }, onSuccess: async () => { setDeleteTarget(null); await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'] }); await queryClient.invalidateQueries({ queryKey: ['auth', 'public-sources'], }); toast.success(t('authSourceDeleted')); }, onError: (error: Error) => { toast.error(error.message || t('deleteAuthSourceFailed')); }, }); 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(t('captchaConfigSaved')); }, onError: (error: Error) => { toast.error(error.message || t('saveConfigFailed')); }, }); const handleCapSave = (e: React.FormEvent) => { e.preventDefault(); saveCapMutation.mutate(); }; return (
{/* 系统登录与注册控制 */}
{t('loginRegistrationSettings')} {t('loginRegistrationSettingsDesc')}
{SECURITY_KEYS.map((item) => { const config = configs[item.key]; const checked = config ? config.value === 'true' : false; const Icon = item.icon; return (
{Icon && } {t(item.titleKey)}

{t(item.descKey)}

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

{t('loginSessionTTLDesc')}

{sessionTTL === 'custom' && ( setCustomHours(e.target.value)} onBlur={handleCustomBlur} onKeyDown={(e) => { if (e.key === 'Enter') { handleCustomBlur(); } }} placeholder={t('hoursPlaceholder')} disabled={updateTTLMutation.isPending} className='w-20 bg-card border-dashed text-xs h-8 px-2' /> )}
{/* 认证源配置管理 */}
{t('authSourceManagement')} {t('authSourceManagementDesc')}
{authSourcesQuery.isPending ? (
) : (authSourcesQuery.data ?? []).length > 0 ? ( (authSourcesQuery.data ?? []).map((source) => (
{source.display_name || source.name} {source.is_active ? t('enabled') : t('disabled')}
{t('identifier')}: {source.name} · {t('type')}:{' '} {source.type.toUpperCase()}
toggleSourceMutation.mutate(source) } />
)) ) : (
{t('noAuthSources')}
)}
{/* 人机验证配置 (Cap CAPTCHA) */}
{t('captchaConfig')} {t('captchaConfigDesc')}
handleToggle('cap_login_enabled', checked) } />
{/* 自动开始计算 Switch */}

{t('autoStartSolving')}

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

{t('challengeCountDesc')}

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

{t('challengeDifficultyDesc')}

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

{t('challengeSizeDesc')}

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

{t('challengeTTLDesc')}

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

{t('tokenTTLDesc')}

setAuthSourceModalOpen(false)} onChanged={async () => { await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'], }); await queryClient.invalidateQueries({ queryKey: ['auth', 'public-sources'], }); await authSourcesQuery.refetch(); }} /> !open && setDeleteTarget(null)} > {t('deleteAuthSourceTitle')} {t('deleteAuthSourceConfirm', { name: deleteTarget?.display_name || deleteTarget?.name || '', })} {tCommon('cancel')} deleteTarget && deleteSourceMutation.mutate(deleteTarget.id) } > {deleteSourceMutation.isPending ? t('deleting') : t('confirmDelete')}
); }