import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, Alert, Badge, Accordion, Textarea } from '@mantine/core'; import { alert } from '@app/components/toast'; import LocalIcon from '@app/components/shared/LocalIcon'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; import { useLoginRequired } from '@app/hooks/useLoginRequired'; import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface SecuritySettingsData { enableLogin?: boolean; csrfDisabled?: boolean; loginMethod?: string; loginAttemptCount?: number; loginResetTimeMinutes?: number; jwt?: { persistence?: boolean; enableKeyRotation?: boolean; enableKeyCleanup?: boolean; keyRetentionDays?: number; secureCookie?: boolean; }; audit?: { enabled?: boolean; level?: number; retentionDays?: number; }; html?: { urlSecurity?: { enabled?: boolean; level?: string; allowedDomains?: string[]; blockedDomains?: string[]; internalTlds?: string[]; blockPrivateNetworks?: boolean; blockLocalhost?: boolean; blockLinkLocal?: boolean; blockCloudMetadata?: boolean; }; }; } export default function AdminSecuritySection() { const { t } = useTranslation(); const { loginEnabled, validateLoginEnabled } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { settings, setSettings, loading, saving, fetchSettings, saveSettings, isFieldPending, } = useAdminSettings({ sectionName: 'security', fetchTransformer: async () => { const [securityResponse, premiumResponse, systemResponse] = await Promise.all([ apiClient.get('/api/v1/admin/settings/section/security'), apiClient.get('/api/v1/admin/settings/section/premium'), apiClient.get('/api/v1/admin/settings/section/system') ]); const securityData = securityResponse.data || {}; const premiumData = premiumResponse.data || {}; const systemData = systemResponse.data || {}; console.log('[AdminSecuritySection] Raw backend data:'); console.log('Security:', JSON.parse(JSON.stringify(securityData))); console.log('Premium:', JSON.parse(JSON.stringify(premiumData))); console.log('System:', JSON.parse(JSON.stringify(systemData))); const { _pending: securityPending, ...securityActive } = securityData; const { _pending: premiumPending, ...premiumActive } = premiumData; const { _pending: systemPending, ...systemActive } = systemData; console.log('[AdminSecuritySection] Extracted pending blocks:', { securityPending: JSON.parse(JSON.stringify(securityPending || {})), premiumPending: JSON.parse(JSON.stringify(premiumPending || {})), systemPending: JSON.parse(JSON.stringify(systemPending || {})) }); const combined: any = { ...securityActive }; // Only add audit if it exists (don't create defaults) if (premiumActive.enterpriseFeatures?.audit) { combined.audit = premiumActive.enterpriseFeatures.audit; } // Only add html if it exists (don't create defaults) if (systemActive.html) { combined.html = systemActive.html; } // Merge all _pending blocks const mergedPending: any = {}; if (securityPending) { Object.assign(mergedPending, securityPending); } if (premiumPending?.enterpriseFeatures?.audit) { mergedPending.audit = premiumPending.enterpriseFeatures.audit; } if (systemPending?.html) { mergedPending.html = systemPending.html; } if (Object.keys(mergedPending).length > 0) { combined._pending = mergedPending; } return combined; }, saveTransformer: (settings) => { const { audit, html, ...securitySettings } = settings; const deltaSettings: Record = { // Security settings 'security.enableLogin': securitySettings.enableLogin, 'security.csrfDisabled': securitySettings.csrfDisabled, 'security.loginMethod': securitySettings.loginMethod, 'security.loginAttemptCount': securitySettings.loginAttemptCount, 'security.loginResetTimeMinutes': securitySettings.loginResetTimeMinutes, // JWT settings 'security.jwt.persistence': securitySettings.jwt?.persistence, 'security.jwt.enableKeyRotation': securitySettings.jwt?.enableKeyRotation, 'security.jwt.enableKeyCleanup': securitySettings.jwt?.enableKeyCleanup, 'security.jwt.keyRetentionDays': securitySettings.jwt?.keyRetentionDays, 'security.jwt.secureCookie': securitySettings.jwt?.secureCookie, // Premium audit settings 'premium.enterpriseFeatures.audit.enabled': audit?.enabled, 'premium.enterpriseFeatures.audit.level': audit?.level, 'premium.enterpriseFeatures.audit.retentionDays': audit?.retentionDays }; // System HTML settings if (html?.urlSecurity) { deltaSettings['system.html.urlSecurity.enabled'] = html.urlSecurity.enabled; deltaSettings['system.html.urlSecurity.level'] = html.urlSecurity.level; deltaSettings['system.html.urlSecurity.allowedDomains'] = html.urlSecurity.allowedDomains; deltaSettings['system.html.urlSecurity.blockedDomains'] = html.urlSecurity.blockedDomains; deltaSettings['system.html.urlSecurity.internalTlds'] = html.urlSecurity.internalTlds; deltaSettings['system.html.urlSecurity.blockPrivateNetworks'] = html.urlSecurity.blockPrivateNetworks; deltaSettings['system.html.urlSecurity.blockLocalhost'] = html.urlSecurity.blockLocalhost; deltaSettings['system.html.urlSecurity.blockLinkLocal'] = html.urlSecurity.blockLinkLocal; deltaSettings['system.html.urlSecurity.blockCloudMetadata'] = html.urlSecurity.blockCloudMetadata; } return { sectionData: {}, deltaSettings }; } }); useEffect(() => { if (loginEnabled) { fetchSettings(); } }, [loginEnabled, fetchSettings]); // Override loading state when login is disabled const actualLoading = loginEnabled ? loading : false; const handleSave = async () => { // Block save if login is disabled if (!validateLoginEnabled()) { return; } try { await saveSettings(); showRestartModal(); } catch (_error) { alert({ alertType: 'error', title: t('admin.error', 'Error'), body: t('admin.settings.saveError', 'Failed to save settings'), }); } }; if (actualLoading) { return ( ); } return (
{t('admin.settings.security.title', 'Security')} {t('admin.settings.security.description', 'Configure authentication, login behaviour, and security policies.')}
{/* Authentication Settings */} {t('admin.settings.security.authentication', 'Authentication')}
{t('admin.settings.security.enableLogin.label', 'Enable Login')} {t('admin.settings.security.enableLogin.description', 'Require users to log in before accessing the application')}
setSettings({ ...settings, enableLogin: e.target.checked })} disabled={!loginEnabled} />
{t('admin.settings.security.htmlUrlSecurity.level.label', 'Security Level')} } description={t('admin.settings.security.htmlUrlSecurity.level.description', 'MAX: whitelist only, MEDIUM: block internal networks, OFF: no restrictions')} value={settings?.html?.urlSecurity?.level || 'MEDIUM'} onChange={(value) => setSettings({ ...settings, html: { ...settings?.html, urlSecurity: { ...settings?.html?.urlSecurity, level: value || 'MEDIUM' } } })} data={[ { value: 'MAX', label: t('admin.settings.security.htmlUrlSecurity.level.max', 'Maximum (Whitelist Only)') }, { value: 'MEDIUM', label: t('admin.settings.security.htmlUrlSecurity.level.medium', 'Medium (Block Internal)') }, { value: 'OFF', label: t('admin.settings.security.htmlUrlSecurity.level.off', 'Off (No Restrictions)') }, ]} comboboxProps={{ zIndex: 1400 }} disabled={!loginEnabled} />
{t('admin.settings.security.htmlUrlSecurity.advanced', 'Advanced Settings')} {/* Allowed Domains */}