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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user