/** * ActivateJailDialog — confirmation dialog for activating an inactive jail. * * Displays the jail name and provides optional override fields for bantime, * findtime, maxretry, port and logpath. Calls the activate endpoint on * confirmation and propagates the result via callbacks. * * Runs pre-activation validation when the dialog opens and displays any * warnings or blocking errors before the user confirms. */ import { useEffect, useState } from "react"; import { Button, Dialog, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, Field, Input, MessageBar, MessageBarBody, MessageBarTitle, Spinner, Text, tokens, } from "@fluentui/react-components"; import { activateJail, validateJailConfig } from "../../api/config"; import type { ActivateJailRequest, InactiveJail, JailValidationIssue, } from "../../types/config"; import { ApiError } from "../../api/client"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface ActivateJailDialogProps { /** The inactive jail to activate, or null when the dialog is closed. */ jail: InactiveJail | null; /** Whether the dialog is currently open. */ open: boolean; /** Called when the dialog should be closed without taking action. */ onClose: () => void; /** Called after the jail has been successfully activated. */ onActivated: () => void; } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- /** * Confirmation dialog for activating an inactive jail. * * All override fields are optional — leaving them blank uses the values * already in the config files. * * @param props - Component props. * @returns JSX element. */ export function ActivateJailDialog({ jail, open, onClose, onActivated, }: ActivateJailDialogProps): React.JSX.Element { const [bantime, setBantime] = useState(""); const [findtime, setFindtime] = useState(""); const [maxretry, setMaxretry] = useState(""); const [port, setPort] = useState(""); const [logpath, setLogpath] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [recoveryStatus, setRecoveryStatus] = useState<"recovered" | "unrecovered" | null>(null); // Pre-activation validation state const [validating, setValidating] = useState(false); const [validationIssues, setValidationIssues] = useState([]); const [validationWarnings, setValidationWarnings] = useState([]); const resetForm = (): void => { setBantime(""); setFindtime(""); setMaxretry(""); setPort(""); setLogpath(""); setError(null); setRecoveryStatus(null); setValidationIssues([]); setValidationWarnings([]); }; // Run pre-validation whenever the dialog opens for a jail. useEffect(() => { if (!open || !jail) return; setValidating(true); setValidationIssues([]); setValidationWarnings([]); validateJailConfig(jail.name) .then((result) => { setValidationIssues(result.issues); }) .catch(() => { // Validation failure is non-blocking — proceed to allow the user to // attempt activation and let the server decide. }) .finally(() => { setValidating(false); }); }, [open, jail]); const handleClose = (): void => { if (submitting) return; resetForm(); onClose(); }; const handleConfirm = (): void => { if (!jail || submitting) return; const overrides: ActivateJailRequest = {}; if (bantime.trim()) overrides.bantime = bantime.trim(); if (findtime.trim()) overrides.findtime = findtime.trim(); if (maxretry.trim()) { const n = parseInt(maxretry.trim(), 10); if (!isNaN(n)) overrides.maxretry = n; } if (port.trim()) overrides.port = port.trim(); if (logpath.trim()) { overrides.logpath = logpath .split("\n") .map((l) => l.trim()) .filter(Boolean); } setSubmitting(true); setError(null); activateJail(jail.name, overrides) .then((result) => { if (!result.active) { if (result.recovered === true) { // Activation failed but the system rolled back automatically. setRecoveryStatus("recovered"); } else if (result.recovered === false) { // Activation failed and rollback also failed. setRecoveryStatus("unrecovered"); } else { // Backend rejected before writing (e.g. missing logpath or filter). // Show the server's message and keep the dialog open. setError(result.message); } return; } if (result.validation_warnings.length > 0) { setValidationWarnings(result.validation_warnings); } resetForm(); onActivated(); }) .catch((err: unknown) => { const msg = err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err); setError(msg); }) .finally(() => { setSubmitting(false); }); }; if (!jail) return <>; // All validation issues block activation — logpath errors are now critical. const blockingIssues = validationIssues; const advisoryIssues: JailValidationIssue[] = []; return ( { if (!data.open) handleClose(); }}> Activate jail “{jail.name}” This will write enabled = true to{" "} jail.d/{jail.name}.local and reload fail2ban. The jail will start monitoring immediately. {/* Pre-validation results */} {validating && (
Validating configuration…
)} {!validating && blockingIssues.length > 0 && ( Configuration errors detected:
    {blockingIssues.map((issue, idx) => (
  • {issue.field}: {issue.message}
  • ))}
)} {!validating && advisoryIssues.length > 0 && ( Advisory warnings:
    {advisoryIssues.map((issue, idx) => (
  • {issue.field}: {issue.message}
  • ))}
)} {validationWarnings.length > 0 && ( Post-activation warnings:
    {validationWarnings.map((w, idx) => (
  • {w}
  • ))}
)} Override values (leave blank to use config defaults)
{ setBantime(d.value); }} /> { setFindtime(d.value); }} /> { setMaxretry(d.value); }} /> { setPort(d.value); }} />
0 ? jail.logpath[0] : "/var/log/example.log" } value={logpath} disabled={submitting} onChange={(_e, d) => { setLogpath(d.value); }} /> {recoveryStatus === "recovered" && ( Activation Failed — Configuration Rolled Back The configuration for jail “{jail.name}” has been rolled back to its previous state and fail2ban is running normally. Review the configuration and try activating again. )} {recoveryStatus === "unrecovered" && ( Activation Failed — Rollback Unsuccessful Activation of jail “{jail.name}” failed and the automatic rollback did not complete. The file{" "} jail.d/{jail.name}.local may still contain{" "} enabled = true. Check the fail2ban logs, correct the file manually, and restart fail2ban. )} {error && ( {error} )}
); }