'use client'; import { useEffect, useState } from 'react'; import { AlertTriangle } from 'lucide-react'; import { SettingsSection, SettingItem, ToggleSwitch } from '@/components/settings/settings-section'; import type { AuditEntry } from '@/lib/admin/types'; interface AdminStatus { enabled: boolean; authenticated: boolean; lastLogin: string | null; passwordChangedAt: string | null; } interface ConfigData { appName?: string; jmapServerUrl?: string; settingsSyncEnabled?: boolean; stalwartFeaturesEnabled?: boolean; oauthEnabled?: boolean; devMode?: boolean; } export default function AdminDashboardPage() { const [status, setStatus] = useState(null); const [recentActivity, setRecentActivity] = useState([]); const [config, setConfig] = useState(null); const [, setConfigSources] = useState | null>(null); const [warnings, setWarnings] = useState([]); const [pluginCount, setPluginCount] = useState(0); const [themeCount, setThemeCount] = useState(0); const [policyRuleCount, setPolicyRuleCount] = useState(0); const [jmapHealth, setJmapHealth] = useState<'unknown' | 'ok' | 'error'>('unknown'); useEffect(() => { fetchDashboardData(); }, []); async function fetchDashboardData() { const [statusRes, auditRes, configRes, adminConfigRes, pluginRes, themeRes, policyRes] = await Promise.all([ fetch('/api/admin/auth'), fetch('/api/admin/audit?limit=10'), fetch('/api/config'), fetch('/api/admin/config'), fetch('/api/admin/plugins').catch(() => null), fetch('/api/admin/themes').catch(() => null), fetch('/api/admin/policy').catch(() => null), ]); if (statusRes.ok) setStatus(await statusRes.json()); if (auditRes.ok) { const data = await auditRes.json(); setRecentActivity(data.entries || []); } let configData: ConfigData | null = null; if (configRes.ok) { configData = await configRes.json(); setConfig(configData); } if (pluginRes?.ok) { const plugins = await pluginRes.json(); setPluginCount(Array.isArray(plugins) ? plugins.length : 0); } if (themeRes?.ok) { const themes = await themeRes.json(); setThemeCount(Array.isArray(themes) ? themes.length : 0); } if (policyRes?.ok) { const policy = await policyRes.json(); const restrictionCount = policy.restrictions ? Object.keys(policy.restrictions).length : 0; const disabledGates = policy.features ? Object.values(policy.features).filter((v: unknown) => !v).length : 0; setPolicyRuleCount(restrictionCount + disabledGates); } if (configData?.jmapServerUrl) { try { const jmapRes = await fetch('/api/config'); setJmapHealth(jmapRes.ok ? 'ok' : 'error'); } catch { setJmapHealth('error'); } } const w: string[] = []; if (adminConfigRes.ok) { const sources = await adminConfigRes.json(); setConfigSources(sources); const sessionSecret = sources?.sessionSecret; if (!sessionSecret?.value || sessionSecret.value === 'your-secret-key-here') { w.push('SESSION_SECRET is not set or using a default value. Sessions are insecure.'); } const adminPassword = sources?.adminPassword; if (adminPassword?.value && adminPassword.source === 'env') { w.push('ADMIN_PASSWORD is still set in environment variables. Remove it now that the hash is stored securely.'); } } setWarnings(w); } const jmapUrl = config?.jmapServerUrl || '—'; const jmapHostname = jmapUrl !== '—' ? (() => { try { return new URL(jmapUrl).hostname; } catch { return jmapUrl; } })() : '—'; return (
{/* Warnings */} {warnings.map((msg, i) => (

{msg}

))} {status && !status.lastLogin && (

First login detected

Remember to remove ADMIN_PASSWORD from your .env file now that the hash is stored securely.

)} {/* Server Info */} {config?.appName || '—'} {jmapHostname} {jmapHealth === 'ok' ? 'Connected' : jmapHealth === 'error' ? 'Error' : 'Unknown'} {status?.lastLogin ? new Date(status.lastLogin).toLocaleString() : 'Never'} {/* Features */} {}} disabled /> {}} disabled /> {}} disabled /> {}} disabled /> {/* Extensions */} {pluginCount} {themeCount} {policyRuleCount} {/* Recent Activity */} {recentActivity.length === 0 ? (
No activity recorded yet
) : ( recentActivity.map((entry, i) => (
{entry.ip} {new Date(entry.ts).toLocaleString()}
)) )}
); } function formatDetail(detail: Record): string { if (!detail || Object.keys(detail).length === 0) return ''; if (detail.key) return `${detail.key}: ${detail.old} → ${detail.new}`; if (detail.reason) return String(detail.reason); if (detail.changes && Array.isArray(detail.changes)) return `${detail.changes.length} setting(s) changed`; return JSON.stringify(detail).slice(0, 80); }