feat: Task 4 — paginated banned-IPs section on jail detail page

Backend:
- Add JailBannedIpsResponse Pydantic model (ban.py)
- Add get_jail_banned_ips() service: server-side pagination, optional
  IP substring search, geo enrichment on page slice only (jail_service.py)
- Add GET /api/jails/{name}/banned endpoint with page/page_size/search
  query params, 400/404/502 error handling (routers/jails.py)
- 23 new tests: 13 service tests + 10 router tests (all passing)

Frontend:
- Add JailBannedIpsResponse TS interface (types/jail.ts)
- Add jailBanned endpoint helper (api/endpoints.ts)
- Add fetchJailBannedIps() API function (api/jails.ts)
- Add BannedIpsSection component: Fluent UI DataGrid, debounced search
  (300 ms), prev/next pagination, page-size dropdown, per-row unban
  button, loading spinner, empty state, error MessageBar (BannedIpsSection.tsx)
- Mount BannedIpsSection in JailDetailPage between stats and patterns
- 12 new Vitest tests for BannedIpsSection (all passing)
This commit is contained in:
2026-03-14 16:28:43 +01:00
parent 0966f347c4
commit baf45c6c62
12 changed files with 1333 additions and 3 deletions

View File

@@ -38,6 +38,7 @@ export const ENDPOINTS = {
// -------------------------------------------------------------------------
jails: "/jails",
jail: (name: string): string => `/jails/${encodeURIComponent(name)}`,
jailBanned: (name: string): string => `/jails/${encodeURIComponent(name)}/banned`,
jailStart: (name: string): string => `/jails/${encodeURIComponent(name)}/start`,
jailStop: (name: string): string => `/jails/${encodeURIComponent(name)}/stop`,
jailIdle: (name: string): string => `/jails/${encodeURIComponent(name)}/idle`,

View File

@@ -10,6 +10,7 @@ import { ENDPOINTS } from "./endpoints";
import type {
ActiveBanListResponse,
IpLookupResponse,
JailBannedIpsResponse,
JailCommandResponse,
JailDetailResponse,
JailListResponse,
@@ -224,3 +225,37 @@ export async function unbanAllBans(): Promise<UnbanAllResponse> {
export async function lookupIp(ip: string): Promise<IpLookupResponse> {
return get<IpLookupResponse>(ENDPOINTS.geoLookup(ip));
}
// ---------------------------------------------------------------------------
// Jail-specific paginated bans
// ---------------------------------------------------------------------------
/**
* Fetch the currently banned IPs for a specific jail, paginated.
*
* Only the requested page is geo-enriched on the backend, so this call
* remains fast even when a jail has thousands of banned IPs.
*
* @param jailName - Jail name (e.g. `"sshd"`).
* @param page - 1-based page number (default 1).
* @param pageSize - Items per page; max 100 (default 25).
* @param search - Optional case-insensitive IP substring filter.
* @returns A {@link JailBannedIpsResponse} with paginated ban entries.
* @throws {ApiError} On non-2xx responses (404 if jail unknown, 502 if fail2ban down).
*/
export async function fetchJailBannedIps(
jailName: string,
page = 1,
pageSize = 25,
search?: string,
): Promise<JailBannedIpsResponse> {
const params: Record<string, string> = {
page: String(page),
page_size: String(pageSize),
};
if (search !== undefined && search !== "") {
params.search = search;
}
const query = new URLSearchParams(params).toString();
return get<JailBannedIpsResponse>(`${ENDPOINTS.jailBanned(jailName)}?${query}`);
}