"use client"; import ConfirmActionDialog from "@/components/ConfirmActionDialog"; import DashboardLayout from "@/components/DashboardLayout"; import IssueDetailsSummary from "@/components/IssueDetailsSummary"; import TablePagination from "@/components/TablePagination"; import ToastViewport from "@/components/ToastViewport"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Flame, Shield, Plus, Trash2, RefreshCw, ToggleLeft, ToggleRight, Loader2, Server as ServerIcon, X, } from "lucide-react"; import { security as securityApi, servers as serversApi, FirewallStatus, SecurityFirewallRule, SecurityOverviewItem, Server, } from "@/lib/api"; import { readCachedPageData, readStoredServerSelection, storeServerSelection, writeCachedPageData, } from "@/lib/page-state"; import { useTablePagination } from "@/lib/use-table-pagination"; import { useToastManager } from "@/lib/use-toast-manager"; const POLL_INTERVAL = 15000; const PAGE_KEY = "firewall"; type PendingConfirmAction = { title: string; description: string; confirmLabel: string; tone?: "danger" | "warning" | "info"; note?: string; onConfirm: () => void; }; export default function FirewallPage() { const { toasts, pushToast, dismissToast } = useToastManager(); const [servers, setServers] = useState([]); const [overview, setOverview] = useState([]); const [firewall, setFirewall] = useState(null); const [selectedServerId, setSelectedServerId] = useState(""); const [loading, setLoading] = useState(true); const [firewallLoading, setFirewallLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(""); const [showAddRule, setShowAddRule] = useState(false); const [ruleForm, setRuleForm] = useState({ rule: "", action: "allow" as "allow" | "deny", from: "", }); const [actionKey, setActionKey] = useState(null); const [confirmDialog, setConfirmDialog] = useState(null); const latestRequestIdRef = useRef(0); const applyCachedState = useCallback((serverId: string) => { const cached = readCachedPageData<{ servers: Server[]; overview: SecurityOverviewItem[]; firewall: FirewallStatus | null; }>(PAGE_KEY, serverId); if (!cached) return false; if (serverId && !cached.firewall) { return false; } setServers(cached.servers); setOverview(cached.overview); setFirewall(cached.firewall); setLoading(false); return true; }, []); const loadData = useCallback( async (preferredServerId?: string, silent = false) => { const requestId = ++latestRequestIdRef.current; if (silent) { setRefreshing(true); } else { setLoading(true); } try { setError(""); const overviewPromise = securityApi.overview(); const serversRes = await serversApi.list(); const nextServers = serversRes.data ?? []; const requestedServerId = preferredServerId ?? ""; const resolvedServerId = nextServers.some( (server) => server.id === requestedServerId, ) ? requestedServerId : ""; if (latestRequestIdRef.current !== requestId) { return; } setServers(nextServers); setSelectedServerId(resolvedServerId); if (!resolvedServerId) { setFirewall(null); setFirewallLoading(false); } else { setFirewallLoading(true); } const [overviewResult] = await Promise.allSettled([overviewPromise]); if (latestRequestIdRef.current !== requestId) { return; } const nextOverview = overviewResult.status === "fulfilled" ? (overviewResult.value.data ?? []) : []; setOverview(nextOverview); if (!resolvedServerId) { storeServerSelection(PAGE_KEY, resolvedServerId); writeCachedPageData( PAGE_KEY, { servers: nextServers, overview: nextOverview, firewall: null, }, resolvedServerId, ); return; } const [firewallResult] = await Promise.allSettled([ securityApi.getFirewall(resolvedServerId), ]); if (latestRequestIdRef.current !== requestId) { return; } let nextFirewall: FirewallStatus | null = null; if (firewallResult.status === "fulfilled") { nextFirewall = firewallResult.value.data ?? null; setFirewall(nextFirewall); } else { setFirewall(null); } setFirewallLoading(false); storeServerSelection(PAGE_KEY, resolvedServerId); writeCachedPageData( PAGE_KEY, { servers: nextServers, overview: nextOverview, firewall: nextFirewall, }, resolvedServerId, ); } catch (err: unknown) { if (latestRequestIdRef.current !== requestId) { return; } setFirewallLoading(false); setError( err instanceof Error ? err.message : "Failed to load firewall data", ); } finally { if (latestRequestIdRef.current === requestId) { setLoading(false); setRefreshing(false); } } }, [], ); useEffect(() => { const storedServerId = readStoredServerSelection(PAGE_KEY); setSelectedServerId(storedServerId); const hasCache = applyCachedState(storedServerId); void loadData(storedServerId, hasCache); }, [applyCachedState, loadData]); useEffect(() => { if (!selectedServerId) return; const interval = window.setInterval(() => { void loadData(selectedServerId, true); }, POLL_INTERVAL); return () => window.clearInterval(interval); }, [loadData, selectedServerId]); const selectedServer = useMemo( () => servers.find((server) => server.id === selectedServerId) ?? null, [selectedServerId, servers], ); const selectedFirewall = useMemo( () => (selectedServerId ? firewall : null), [firewall, selectedServerId], ); const summary = useMemo( () => ({ enabledServers: overview.filter((item) => item.firewall.enabled).length, totalRules: overview.reduce( (sum, item) => sum + item.firewall.rulesCount, 0, ), totalServers: overview.length, }), [overview], ); const rulesPagination = useTablePagination({ items: selectedFirewall?.rules ?? [], resetKey: selectedServerId, }); const handleSelectAllServers = () => { storeServerSelection(PAGE_KEY, ""); setSelectedServerId(""); setFirewall(null); setFirewallLoading(false); const hasCache = applyCachedState(""); void loadData("", hasCache); }; const handleSelectServer = (serverId: string) => { storeServerSelection(PAGE_KEY, serverId); setSelectedServerId(serverId); const hasCache = applyCachedState(serverId); if (!hasCache) { setFirewall(null); } setFirewallLoading(true); void loadData(serverId, true); }; const toggleFirewall = async () => { if (!selectedServerId || !selectedFirewall) return; setActionKey("toggle-firewall"); try { if (selectedFirewall.enabled) { await securityApi.disableFirewall(selectedServerId); } else { await securityApi.enableFirewall(selectedServerId); } await loadData(selectedServerId, true); pushToast({ tone: "success", title: "Firewall", message: `Firewall ${selectedFirewall.enabled ? "disabled" : "enabled"} successfully`, showProgress: true, }); } catch (err: unknown) { pushToast({ tone: "error", title: "Firewall", message: err instanceof Error ? err.message : "Failed to update firewall state", showProgress: true, }); } finally { setActionKey(null); } }; const addRule = async () => { if (!selectedServerId || !ruleForm.rule.trim()) return; setActionKey("add-rule"); try { await securityApi.addFirewallRule(selectedServerId, { rule: ruleForm.rule.trim(), action: ruleForm.action, from: ruleForm.from.trim() || undefined, }); setRuleForm({ rule: "", action: "allow", from: "" }); setShowAddRule(false); await loadData(selectedServerId, true); pushToast({ tone: "success", title: "Firewall Rule", message: "Firewall rule added successfully", showProgress: true, }); } catch (err: unknown) { pushToast({ tone: "error", title: "Firewall Rule", message: err instanceof Error ? err.message : "Failed to add firewall rule", showProgress: true, }); } finally { setActionKey(null); } }; const removeRule = async (ruleNum: number) => { if (!selectedServerId) return; setActionKey(`delete-rule-${ruleNum}`); try { await securityApi.deleteFirewallRule(selectedServerId, ruleNum); await loadData(selectedServerId, true); pushToast({ tone: "success", title: "Firewall Rule", message: `Firewall rule #${ruleNum} deleted successfully`, showProgress: true, }); } catch (err: unknown) { pushToast({ tone: "error", title: "Firewall Rule", message: err instanceof Error ? err.message : "Failed to delete firewall rule", showProgress: true, }); } finally { setActionKey(null); } }; const toggleRule = async (rule: SecurityFirewallRule) => { if (!selectedServerId) return; const actionId = `toggle-rule-${rule.id ?? rule.number}`; setActionKey(actionId); try { if (rule.enabled) { if (rule.number === null) return; await securityApi.disableFirewallRule(selectedServerId, rule.number, { rule: rule.rule, action: rule.action.toLowerCase() as "allow" | "deny", direction: rule.direction, from: rule.from, description: rule.description, }); } else { if (!rule.id) return; await securityApi.enableSavedFirewallRule(selectedServerId, rule.id); } await loadData(selectedServerId, true); pushToast({ tone: "success", title: "Firewall Rule", message: `Firewall rule ${rule.enabled ? "disabled" : "enabled"} successfully`, showProgress: true, }); } catch (err: unknown) { pushToast({ tone: "error", title: "Firewall Rule", message: err instanceof Error ? err.message : "Failed to toggle firewall rule", showProgress: true, }); } finally { setActionKey(null); } }; const removeSavedRule = async (ruleId: string) => { if (!selectedServerId) return; setActionKey(`delete-saved-rule-${ruleId}`); try { await securityApi.deleteSavedFirewallRule(selectedServerId, ruleId); await loadData(selectedServerId, true); pushToast({ tone: "success", title: "Firewall Rule", message: "Saved firewall rule deleted successfully", showProgress: true, }); } catch (err: unknown) { pushToast({ tone: "error", title: "Firewall Rule", message: err instanceof Error ? err.message : "Failed to delete saved firewall rule", showProgress: true, }); } finally { setActionKey(null); } }; const deleteRule = async (ruleNum: number) => { setConfirmDialog({ title: "Delete Firewall Rule", description: `Delete firewall rule #${ruleNum}?`, confirmLabel: "Delete Rule", tone: "danger", note: "Traffic matching this rule will no longer be filtered by UFW.", onConfirm: () => { void removeRule(ruleNum); }, }); }; const deleteSavedRule = async (ruleId: string) => { setConfirmDialog({ title: "Delete Saved Firewall Rule", description: "Delete this saved disabled firewall rule?", confirmLabel: "Delete Saved Rule", tone: "danger", note: "This preset will be removed from the saved rules list.", onConfirm: () => { void removeSavedRule(ruleId); }, }); }; return ( setConfirmDialog(null)} onConfirm={() => { const current = confirmDialog; setConfirmDialog(null); current?.onConfirm(); }} />
{error && ( )}
{[ { label: "Firewall Enabled", value: `${summary.enabledServers}/${summary.totalServers}`, color: "#10b981", icon: Shield, }, { label: "Total Rules", value: summary.totalRules, color: "#f59e0b", icon: Flame, }, { label: "Realtime Poll", value: "15s", color: "#3b82f6", icon: RefreshCw, }, ].map((item) => { const Icon = item.icon; return (

{item.value}

{item.label}

); })}
Server:
{servers.map((server) => ( ))}
{loading ? (

Loading firewall status...

) : !selectedServerId ? (
Select a server to inspect firewall rules. Overview all servers is shown below.
) : firewallLoading && !selectedFirewall ? (

Loading selected server firewall details...

) : !selectedFirewall || !selectedServer ? (
Unable to load firewall details for the selected server.
) : (

Firewall (UFW)

{selectedServer.name} · {selectedServer.ip}

{selectedFirewall.enabled ? "Enabled" : "Disabled"}
{selectedFirewall.rules.length === 0 ? ( ) : ( rulesPagination.paginatedItems.map((rule) => ( )) )}
# Status Rule Action From
No firewall rules detected.
{rule.number === null ? "Saved" : `#${rule.number}`} {rule.enabled ? "ACTIVE" : "DISABLED"} {rule.rule} {rule.action} {rule.from}
)}

Firewall Overview — All Servers

{overview.map((item) => (
{item.server.name} {item.firewall.enabled ? "Enabled" : item.error ? "Error" : "Disabled"}
Rules {item.firewall.rulesCount}
Fail2ban {!item.fail2ban.installed ? "Not installed" : item.fail2ban.enabled ? "Active" : "Inactive"}
IP {item.server.ip}
))}
{showAddRule && (
setShowAddRule(false)}>
event.stopPropagation()} >

Add Firewall Rule

Configure a new UFW rule for this server.

setRuleForm((current) => ({ ...current, rule: event.target.value, })) } placeholder="22/tcp or 8080" />
setRuleForm((current) => ({ ...current, from: event.target.value, })) } placeholder="Anywhere or 1.2.3.4" />
)}
); }