Files
BanGUI/frontend/src/components/config/ActivateJailDialog.tsx
Lukas 12f04bd8d6 Remove RecoveryBanner component and dead onCrashDetected code
- Delete RecoveryBanner.tsx component and its test
- Remove RecoveryBanner from MainLayout
- Remove onCrashDetected prop from ActivateJailDialog, JailsTab
- Remove fetchPendingRecovery, rollbackJail API functions
- Remove configJailRollback, configPendingRecovery endpoints
- Remove PendingRecovery type
2026-03-15 13:41:06 +01:00

382 lines
13 KiB
TypeScript

/**
* 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<string | null>(null);
const [recoveryStatus, setRecoveryStatus] = useState<"recovered" | "unrecovered" | null>(null);
// Pre-activation validation state
const [validating, setValidating] = useState(false);
const [validationIssues, setValidationIssues] = useState<JailValidationIssue[]>([]);
const [validationWarnings, setValidationWarnings] = useState<string[]>([]);
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 (
<Dialog open={open} onOpenChange={(_ev, data) => { if (!data.open) handleClose(); }}>
<DialogSurface>
<DialogBody>
<DialogTitle>Activate jail &ldquo;{jail.name}&rdquo;</DialogTitle>
<DialogContent>
<Text block style={{ marginBottom: tokens.spacingVerticalM }}>
This will write <code>enabled = true</code> to{" "}
<code>jail.d/{jail.name}.local</code> and reload fail2ban. The
jail will start monitoring immediately.
</Text>
{/* Pre-validation results */}
{validating && (
<div style={{ marginBottom: tokens.spacingVerticalS, display: "flex", alignItems: "center", gap: tokens.spacingHorizontalS }}>
<Spinner size="tiny" />
<Text size={200}>Validating configuration</Text>
</div>
)}
{!validating && blockingIssues.length > 0 && (
<MessageBar
intent="error"
style={{ marginBottom: tokens.spacingVerticalS }}
>
<MessageBarBody>
<strong>Configuration errors detected:</strong>
<ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}>
{blockingIssues.map((issue, idx) => (
<li key={idx}><em>{issue.field}:</em> {issue.message}</li>
))}
</ul>
</MessageBarBody>
</MessageBar>
)}
{!validating && advisoryIssues.length > 0 && (
<MessageBar
intent="warning"
style={{ marginBottom: tokens.spacingVerticalS }}
>
<MessageBarBody>
<strong>Advisory warnings:</strong>
<ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}>
{advisoryIssues.map((issue, idx) => (
<li key={idx}><em>{issue.field}:</em> {issue.message}</li>
))}
</ul>
</MessageBarBody>
</MessageBar>
)}
{validationWarnings.length > 0 && (
<MessageBar
intent="warning"
style={{ marginBottom: tokens.spacingVerticalS }}
>
<MessageBarBody>
<strong>Post-activation warnings:</strong>
<ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}>
{validationWarnings.map((w, idx) => (
<li key={idx}>{w}</li>
))}
</ul>
</MessageBarBody>
</MessageBar>
)}
<Text block weight="semibold" style={{ marginBottom: tokens.spacingVerticalS }}>
Override values (leave blank to use config defaults)
</Text>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: tokens.spacingVerticalS,
}}
>
<Field
label="Ban time"
hint={jail.bantime ? `Current: ${jail.bantime}` : undefined}
>
<Input
placeholder={jail.bantime ?? "e.g. 10m"}
value={bantime}
disabled={submitting}
onChange={(_e, d) => { setBantime(d.value); }}
/>
</Field>
<Field
label="Find time"
hint={jail.findtime ? `Current: ${jail.findtime}` : undefined}
>
<Input
placeholder={jail.findtime ?? "e.g. 10m"}
value={findtime}
disabled={submitting}
onChange={(_e, d) => { setFindtime(d.value); }}
/>
</Field>
<Field
label="Max retry"
hint={jail.maxretry != null ? `Current: ${String(jail.maxretry)}` : undefined}
>
<Input
type="number"
placeholder={jail.maxretry != null ? String(jail.maxretry) : "e.g. 5"}
value={maxretry}
disabled={submitting}
onChange={(_e, d) => { setMaxretry(d.value); }}
/>
</Field>
<Field
label="Port"
hint={jail.port ? `Current: ${jail.port}` : undefined}
>
<Input
placeholder={jail.port ?? "e.g. ssh"}
value={port}
disabled={submitting}
onChange={(_e, d) => { setPort(d.value); }}
/>
</Field>
</div>
<Field
label="Log path(s)"
hint="One path per line; leave blank to use config defaults."
style={{ marginTop: tokens.spacingVerticalS }}
>
<Input
placeholder={
jail.logpath.length > 0 ? jail.logpath[0] : "/var/log/example.log"
}
value={logpath}
disabled={submitting}
onChange={(_e, d) => { setLogpath(d.value); }}
/>
</Field>
{recoveryStatus === "recovered" && (
<MessageBar
intent="warning"
style={{ marginTop: tokens.spacingVerticalS }}
>
<MessageBarBody>
<MessageBarTitle>Activation Failed Configuration Rolled Back</MessageBarTitle>
The configuration for jail &ldquo;{jail.name}&rdquo; has been
rolled back to its previous state and fail2ban is running
normally. Review the configuration and try activating again.
</MessageBarBody>
</MessageBar>
)}
{recoveryStatus === "unrecovered" && (
<MessageBar
intent="error"
style={{ marginTop: tokens.spacingVerticalS }}
>
<MessageBarBody>
<MessageBarTitle>Activation Failed Rollback Unsuccessful</MessageBarTitle>
Activation of jail &ldquo;{jail.name}&rdquo; failed and the
automatic rollback did not complete. The file{" "}
<code>jail.d/{jail.name}.local</code> may still contain{" "}
<code>enabled = true</code>. Check the fail2ban logs, correct
the file manually, and restart fail2ban.
</MessageBarBody>
</MessageBar>
)}
{error && (
<MessageBar
intent="error"
style={{ marginTop: tokens.spacingVerticalS }}
>
<MessageBarBody>{error}</MessageBarBody>
</MessageBar>
)}
</DialogContent>
<DialogActions>
<Button
appearance="secondary"
onClick={handleClose}
disabled={submitting}
>
Cancel
</Button>
<Button
appearance="primary"
onClick={handleConfirm}
disabled={submitting || validating || blockingIssues.length > 0}
icon={submitting ? <Spinner size="tiny" /> : undefined}
>
{submitting ? "Activating and verifying…" : "Activate"}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
}