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:
@@ -4,6 +4,7 @@ Provides CRUD and control operations for fail2ban jails:
|
||||
|
||||
* ``GET /api/jails`` — list all jails
|
||||
* ``GET /api/jails/{name}`` — full detail for one jail
|
||||
* ``GET /api/jails/{name}/banned`` — paginated currently-banned IPs for one jail
|
||||
* ``POST /api/jails/{name}/start`` — start a jail
|
||||
* ``POST /api/jails/{name}/stop`` — stop a jail
|
||||
* ``POST /api/jails/{name}/idle`` — toggle idle mode
|
||||
@@ -23,6 +24,7 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Body, HTTPException, Path, Request, status
|
||||
|
||||
from app.dependencies import AuthDep
|
||||
from app.models.ban import JailBannedIpsResponse
|
||||
from app.models.jail import (
|
||||
IgnoreIpRequest,
|
||||
JailCommandResponse,
|
||||
@@ -540,3 +542,74 @@ async def toggle_ignore_self(
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Currently banned IPs (paginated)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{name}/banned",
|
||||
response_model=JailBannedIpsResponse,
|
||||
summary="Return paginated currently-banned IPs for a single jail",
|
||||
)
|
||||
async def get_jail_banned_ips(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
name: _NamePath,
|
||||
page: int = 1,
|
||||
page_size: int = 25,
|
||||
search: str | None = None,
|
||||
) -> JailBannedIpsResponse:
|
||||
"""Return a paginated list of IPs currently banned by a specific jail.
|
||||
|
||||
The full ban list is fetched from the fail2ban socket, filtered by the
|
||||
optional *search* substring, sliced to the requested page, and then
|
||||
geo-enriched exclusively for that page slice.
|
||||
|
||||
Args:
|
||||
request: Incoming request (used to access ``app.state``).
|
||||
_auth: Validated session — enforces authentication.
|
||||
name: Jail name.
|
||||
page: 1-based page number (default 1, min 1).
|
||||
page_size: Items per page (default 25, max 100).
|
||||
search: Optional case-insensitive substring filter on the IP address.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.ban.JailBannedIpsResponse` with the paginated bans.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 when *page* or *page_size* are out of range.
|
||||
HTTPException: 404 when the jail does not exist.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
if page < 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="page must be >= 1.",
|
||||
)
|
||||
if not (1 <= page_size <= 100):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="page_size must be between 1 and 100.",
|
||||
)
|
||||
|
||||
socket_path: str = request.app.state.settings.fail2ban_socket
|
||||
http_session = getattr(request.app.state, "http_session", None)
|
||||
app_db = getattr(request.app.state, "db", None)
|
||||
|
||||
try:
|
||||
return await jail_service.get_jail_banned_ips(
|
||||
socket_path=socket_path,
|
||||
jail_name=name,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
http_session=http_session,
|
||||
app_db=app_db,
|
||||
)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
|
||||
Reference in New Issue
Block a user