import { useState, useEffect, useCallback } from 'react' import { Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, CircularProgress, Container, FormControl, FormControlLabel, Grid, IconButton, InputLabel, MenuItem, Select, Snackbar, Switch, TextField, Toolbar, Typography, } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import SaveIcon from '@mui/icons-material/Save' import DeleteIcon from '@mui/icons-material/Delete' import AddIcon from '@mui/icons-material/Add' import CloudIcon from '@mui/icons-material/Cloud' import EmailIcon from '@mui/icons-material/Email' import VerifiedUserIcon from '@mui/icons-material/VerifiedUser' import { settingsApi } from '../api/client' import type { AzureSsoConfig, SmtpConfig, PollingConfig, NotificationConfig } from '../types' type AzureSsoForm = AzureSsoConfig & { client_secret?: string } type SmtpForm = SmtpConfig & { password?: string } export default function SettingsPage() { const [azureSso, setAzureSso] = useState({ enabled: false, tenant_id: '', client_id: '', client_secret: '', redirect_uri: '', scopes: 'openid email profile', }) const [smtp, setSmtp] = useState({ enabled: false, host: '', port: 587, username: '', password: '', from: '', tls_mode: 'starttls', }) const [polling, setPolling] = useState({ health_poll_interval_secs: 300, patch_poll_interval_secs: 1800, }) const [ipWhitelist, setIpWhitelist] = useState([]) const [webTlsStrategy, setWebTlsStrategy] = useState('internal_ca') const [notification, setNotification] = useState({ email_enabled: false, email_from: 'patch-manager@localhost', recipients: [], }) const [saving, setSaving] = useState(false) const [testingAzure, setTestingAzure] = useState(false) const [testingSmtp, setTestingSmtp] = useState(false) const [testingIntegrity, setTestingIntegrity] = useState(false) const [integrityResult, setIntegrityResult] = useState<{ intact: boolean; rows_checked: number; errors: Array<{ row_id: number; expected_hash: string; actual_hash: string }> } | null>(null) const [azureSsoTestResult, setAzureSsoTestResult] = useState<{ success: boolean; message: string } | null>(null) const [smtpTestResult, setSmtpTestResult] = useState<{ success: boolean; message: string } | null>(null) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) const [loading, setLoading] = useState(true) const loadSettings = useCallback(async () => { try { setLoading(true) const { data } = await settingsApi.get() setAzureSso({ ...data.azure_sso, client_secret: '' }) setSmtp({ ...data.smtp, password: '' }) setPolling(data.polling) setIpWhitelist(data.ip_whitelist) setWebTlsStrategy(data.web_tls_strategy) setNotification(data.notification) } catch { setError('Failed to load settings') } finally { setLoading(false) } }, []) useEffect(() => { loadSettings() }, [loadSettings]) const handleSave = async () => { setSaving(true) setError(null) setSuccess(null) try { await settingsApi.update({ azure_sso: { ...azureSso }, smtp: { ...smtp }, polling, ip_whitelist: ipWhitelist, web_tls_strategy: webTlsStrategy, notification, }) setSuccess('Settings saved successfully') } catch { setError('Failed to save settings') } finally { setSaving(false) } } const handleAuditIntegrity = async () => { setTestingIntegrity(true) setIntegrityResult(null) try { const { data } = await settingsApi.auditIntegrity() setIntegrityResult(data) } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Verification failed' setIntegrityResult({ intact: false, rows_checked: 0, errors: [{ row_id: 0, expected_hash: '', actual_hash: msg }] }) } finally { setTestingIntegrity(false) } } const handleTestAzureSso = async () => { setTestingAzure(true) setAzureSsoTestResult(null) try { const { data } = await settingsApi.testAzureSso() setAzureSsoTestResult(data) } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Test failed' setAzureSsoTestResult({ success: false, message: msg }) } finally { setTestingAzure(false) } } const handleTestSmtp = async () => { setTestingSmtp(true) setSmtpTestResult(null) try { const { data } = await settingsApi.testSmtp() setSmtpTestResult(data) } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Test failed' setSmtpTestResult({ success: false, message: msg }) } finally { setTestingSmtp(false) } } const addWhitelistEntry = () => setIpWhitelist([...ipWhitelist, '']) const removeWhitelistEntry = (idx: number) => setIpWhitelist(ipWhitelist.filter((_, i) => i !== idx)) const updateWhitelistEntry = (idx: number, value: string) => { const updated = [...ipWhitelist] updated[idx] = value setIpWhitelist(updated) } if (loading) { return ( ) } return ( Settings {error && setError(null)}>{error}} {/* Section 1: Azure SSO Configuration */} }> Azure SSO Configuration setAzureSso({ ...azureSso, enabled: e.target.checked })} />} label="Enable Azure SSO" /> setAzureSso({ ...azureSso, tenant_id: e.target.value })} /> setAzureSso({ ...azureSso, client_id: e.target.value })} /> setAzureSso({ ...azureSso, client_secret: e.target.value })} placeholder="Enter new secret or leave masked" /> setAzureSso({ ...azureSso, redirect_uri: e.target.value })} helperText="e.g. https://patch-manager.example.com/api/v1/auth/azure/callback" /> setAzureSso({ ...azureSso, scopes: e.target.value })} /> {azureSsoTestResult && ( {azureSsoTestResult.message} )} {/* Section 2: SMTP Configuration */} }> SMTP Configuration setSmtp({ ...smtp, enabled: e.target.checked })} />} label="Enable Email Notifications" /> setSmtp({ ...smtp, host: e.target.value })} /> setSmtp({ ...smtp, port: Number(e.target.value) })} /> TLS Mode setSmtp({ ...smtp, username: e.target.value })} /> setSmtp({ ...smtp, password: e.target.value })} placeholder="Enter new password or leave masked" /> setSmtp({ ...smtp, from: e.target.value })} helperText="noreply@example.com" /> {smtpTestResult && ( {smtpTestResult.message} )} {/* Section 3: Polling Intervals */} }> Polling Intervals setPolling({ ...polling, health_poll_interval_secs: Number(e.target.value) })} helperText="How often to check agent health (default: 300)" /> setPolling({ ...polling, patch_poll_interval_secs: Number(e.target.value) })} helperText="How often to check for patch updates (default: 1800)" /> {/* Section 4: IP Whitelist */} }> IP Whitelist Restrict access to specific IP addresses or CIDR ranges. Leave empty to allow all. {ipWhitelist.map((entry, idx) => ( updateWhitelistEntry(idx, e.target.value)} placeholder="10.0.0.0/8 or 192.168.1.100" sx={{ flexGrow: 1 }} /> removeWhitelistEntry(idx)}> ))} {/* Section 5: Web UI TLS Certificate Strategy */} }> Web UI TLS Certificate TLS Certificate Strategy {webTlsStrategy === 'internal_ca' ? 'The internal CA will automatically generate and renew the web UI TLS certificate.' : 'You must provide your own TLS certificate and key files at the configured paths.'} {/* Section 6: Email Notification Settings */} }> Email Notifications setNotification({ ...notification, email_enabled: e.target.checked })} />} label="Enable Email Notifications" /> setNotification({ ...notification, email_from: e.target.value })} helperText="Sender address for notifications" /> Recipients {notification.recipients.map((email, idx) => ( { const updated = [...notification.recipients] updated[idx] = e.target.value setNotification({ ...notification, recipients: updated }) }} placeholder="admin@example.com" sx={{ flexGrow: 1 }} /> { setNotification({ ...notification, recipients: notification.recipients.filter((_, i) => i !== idx) }) }}> ))} {/* Section 7: Audit Integrity Verification */} }> Audit Integrity Verification Verify the integrity of the audit log hash chain. This checks that all audit entries are properly linked and have not been tampered with. {integrityResult && ( {integrityResult.intact ? `Audit chain intact — ${integrityResult.rows_checked} rows verified` : `Audit chain compromised! ${integrityResult.errors.length} error(s) found across ${integrityResult.rows_checked} rows checked`} {integrityResult.errors.length > 0 && ( {integrityResult.errors.slice(0, 5).map((e, i) => ( Row {e.row_id}: expected {e.expected_hash.substring(0, 16)}... got {e.actual_hash.substring(0, 16)}... ))} {integrityResult.errors.length > 5 && ( ...and {integrityResult.errors.length - 5} more errors )} )} )} setSuccess(null)} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setSuccess(null)}>{success} ) }