- 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
241 lines
7.4 KiB
TypeScript
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 “{jail.name}”</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>
|
|
);
|
|
}
|