Files
BanGUI/frontend/src/components/config/ActivateJailDialog.tsx
Lukas 8d9d63b866 feat(stage-1): inactive jail discovery and activation
- Backend: config_file_service.py parses jail.conf/jail.local/jail.d/*
  following fail2ban merge order; discovers jails not running in fail2ban
- Backend: 3 new API endpoints (GET /jails/inactive, POST /jails/{name}/activate,
  POST /jails/{name}/deactivate); moved /jails/inactive before /jails/{name}
  to fix route-ordering conflict
- Frontend: ActivateJailDialog component with optional parameter overrides
- Frontend: JailsTab extended with inactive jail list and InactiveJailDetail pane
- Frontend: JailsPage JailOverviewSection shows inactive jails with toggle
- Tests: 57 service tests + 16 router tests for all new endpoints (all pass)
- Docs: Features.md, Architekture.md, Tasks.md updated; Tasks 1.1-1.5 marked done
2026-03-13 15:44:36 +01:00

241 lines
7.4 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.
*/
import { useState } from "react";
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
Field,
Input,
MessageBar,
MessageBarBody,
Spinner,
Text,
tokens,
} from "@fluentui/react-components";
import { activateJail } from "../../api/config";
import type { ActivateJailRequest, InactiveJail } 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 resetForm = (): void => {
setBantime("");
setFindtime("");
setMaxretry("");
setPort("");
setLogpath("");
setError(null);
};
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(() => {
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 <></>;
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>
<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>
{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}
icon={submitting ? <Spinner size="tiny" /> : undefined}
>
{submitting ? "Activating…" : "Activate"}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
}