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
This commit is contained in:
240
frontend/src/components/config/ActivateJailDialog.tsx
Normal file
240
frontend/src/components/config/ActivateJailDialog.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user