Add ban management features and update documentation

- Implement ban model, service, and router endpoints in backend
- Add ban table component and dashboard integration in frontend
- Update ban-related types and API endpoints
- Add comprehensive tests for ban service and dashboard router
- Update documentation (Features, Tasks, Architecture, Web-Design)
- Clean up old fail2ban configuration files
- Update Makefile with new commands
This commit is contained in:
2026-03-06 20:33:42 +01:00
parent 06738dbfa5
commit cbad4ea706
20 changed files with 58 additions and 760 deletions

View File

@@ -1,13 +1,11 @@
/**
* `BanTable` component.
*
* Renders a Fluent UI v9 `DataGrid` for the dashboard ban-list and
* access-list views. Uses the {@link useBans} hook to fetch and manage
* paginated data from the backend.
* Renders a Fluent UI v9 `DataGrid` for the dashboard ban-list view.
* Uses the {@link useBans} hook to fetch and manage paginated data from
* the backend.
*
* Columns differ between modes:
* - `"bans"` — Time, IP, Service, Country, Jail, Ban Count.
* - `"accesses"` — Time, IP, Log Line, Country, Jail.
* Columns: Time, IP, Service, Country, Jail, Ban Count.
*/
import {
@@ -28,8 +26,8 @@ import {
} from "@fluentui/react-components";
import { PageEmpty, PageError, PageLoading } from "./PageFeedback";
import { ChevronLeftRegular, ChevronRightRegular } from "@fluentui/react-icons";
import { useBans, type BanTableMode } from "../hooks/useBans";
import type { AccessListItem, DashboardBanItem, TimeRange } from "../types/ban";
import { useBans } from "../hooks/useBans";
import type { DashboardBanItem, TimeRange } from "../types/ban";
// ---------------------------------------------------------------------------
// Types
@@ -37,8 +35,6 @@ import type { AccessListItem, DashboardBanItem, TimeRange } from "../types/ban";
/** Props for the {@link BanTable} component. */
interface BanTableProps {
/** Whether to render ban records or individual access events. */
mode: BanTableMode;
/**
* Active time-range preset — controlled by the parent `DashboardPage`.
* Changing this value triggers a re-fetch.
@@ -179,68 +175,20 @@ function buildBanColumns(styles: ReturnType<typeof useStyles>): TableColumnDefin
];
}
/** Columns for the access-list view (`mode === "accesses"`). */
function buildAccessColumns(styles: ReturnType<typeof useStyles>): TableColumnDefinition<AccessListItem>[] {
return [
createTableColumn<AccessListItem>({
columnId: "timestamp",
renderHeaderCell: () => "Timestamp",
renderCell: (item) => (
<Text size={200}>{formatTimestamp(item.timestamp)}</Text>
),
}),
createTableColumn<AccessListItem>({
columnId: "ip",
renderHeaderCell: () => "IP Address",
renderCell: (item) => (
<span className={styles.mono}>{item.ip}</span>
),
}),
createTableColumn<AccessListItem>({
columnId: "line",
renderHeaderCell: () => "Log Line",
renderCell: (item) => (
<Tooltip content={item.line} relationship="description">
<span className={`${styles.mono} ${styles.truncate}`}>{item.line}</span>
</Tooltip>
),
}),
createTableColumn<AccessListItem>({
columnId: "country",
renderHeaderCell: () => "Country",
renderCell: (item) => (
<Text size={200}>
{item.country_name ?? item.country_code ?? "—"}
</Text>
),
}),
createTableColumn<AccessListItem>({
columnId: "jail",
renderHeaderCell: () => "Jail",
renderCell: (item) => <Text size={200}>{item.jail}</Text>,
}),
];
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
/**
* Data table for the dashboard ban-list and access-list views.
* Data table for the dashboard ban-list view.
*
* @param props.mode - `"bans"` or `"accesses"`.
* @param props.timeRange - Active time-range preset from the parent page.
*/
export function BanTable({ mode, timeRange }: BanTableProps): React.JSX.Element {
export function BanTable({ timeRange }: BanTableProps): React.JSX.Element {
const styles = useStyles();
const { banItems, accessItems, total, page, setPage, loading, error, refresh } = useBans(
mode,
timeRange,
);
const { banItems, total, page, setPage, loading, error, refresh } = useBans(timeRange);
const banColumns = buildBanColumns(styles);
const accessColumns = buildAccessColumns(styles);
// --------------------------------------------------------------------------
// Loading state
@@ -259,15 +207,8 @@ export function BanTable({ mode, timeRange }: BanTableProps): React.JSX.Element
// --------------------------------------------------------------------------
// Empty state
// --------------------------------------------------------------------------
const isEmpty = mode === "bans" ? banItems.length === 0 : accessItems.length === 0;
if (isEmpty) {
return (
<PageEmpty
message={`No ${
mode === "bans" ? "bans" : "accesses"
} recorded in the selected time window.`}
/>
);
if (banItems.length === 0) {
return <PageEmpty message="No bans recorded in the selected time window." />;
}
// --------------------------------------------------------------------------
@@ -279,68 +220,15 @@ export function BanTable({ mode, timeRange }: BanTableProps): React.JSX.Element
const hasNext = page < totalPages;
// --------------------------------------------------------------------------
// Render — bans mode
// --------------------------------------------------------------------------
if (mode === "bans") {
return (
<div className={styles.root}>
<div className={styles.tableWrapper}>
<DataGrid
items={banItems}
columns={banColumns}
getRowId={(item: DashboardBanItem) => `${item.ip}:${item.jail}:${item.banned_at}`}
>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => (
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody<DashboardBanItem>>
{({ item, rowId }) => (
<DataGridRow<DashboardBanItem> key={rowId}>
{({ renderCell }) => (
<DataGridCell>{renderCell(item)}</DataGridCell>
)}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
</div>
<div className={styles.pagination}>
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
{total} total · Page {page} of {totalPages}
</Text>
<Button
icon={<ChevronLeftRegular />}
appearance="subtle"
disabled={!hasPrev}
onClick={() => { setPage(page - 1); }}
aria-label="Previous page"
/>
<Button
icon={<ChevronRightRegular />}
appearance="subtle"
disabled={!hasNext}
onClick={() => { setPage(page + 1); }}
aria-label="Next page"
/>
</div>
</div>
);
}
// --------------------------------------------------------------------------
// Render — accesses mode
// Render
// --------------------------------------------------------------------------
return (
<div className={styles.root}>
<div className={styles.tableWrapper}>
<DataGrid
items={accessItems}
columns={accessColumns}
getRowId={(item: AccessListItem) => `${item.ip}:${item.jail}:${item.timestamp}:${item.line.slice(0, 40)}`}
items={banItems}
columns={banColumns}
getRowId={(item: DashboardBanItem) => `${item.ip}:${item.jail}:${item.banned_at}`}
>
<DataGridHeader>
<DataGridRow>
@@ -349,9 +237,9 @@ export function BanTable({ mode, timeRange }: BanTableProps): React.JSX.Element
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody<AccessListItem>>
<DataGridBody<DashboardBanItem>>
{({ item, rowId }) => (
<DataGridRow<AccessListItem> key={rowId}>
<DataGridRow<DashboardBanItem> key={rowId}>
{({ renderCell }) => (
<DataGridCell>{renderCell(item)}</DataGridCell>
)}