Add origin field and filter for ban sources (Tasks 1 & 2)

- Task 1: Mark imported blocklist IP addresses
  - Add BanOrigin type and _derive_origin() to ban.py model
  - Populate origin field in ban_service list_bans() and bans_by_country()
  - BanTable and MapPage companion table show origin badge column
  - Tests: origin derivation in test_ban_service.py and test_dashboard.py

- Task 2: Add origin filter to dashboard and world map
  - ban_service: _origin_sql_filter() helper; origin param on list_bans()
    and bans_by_country()
  - dashboard router: optional origin query param forwarded to service
  - Frontend: BanOriginFilter type + BAN_ORIGIN_FILTER_LABELS in ban.ts
  - fetchBans / fetchBansByCountry forward origin to API
  - useBans / useMapData accept and pass origin; page resets on change
  - BanTable accepts origin prop; DashboardPage adds segmented filter
  - MapPage adds origin Select next to time-range picker
  - Tests: origin filter assertions in test_ban_service and test_dashboard
This commit is contained in:
2026-03-07 20:03:43 +01:00
parent 706d2e1df8
commit 53d664de4f
28 changed files with 1637 additions and 103 deletions

View File

@@ -48,6 +48,26 @@ class UnbanRequest(BaseModel):
)
#: Discriminator literal for the origin of a ban.
BanOrigin = Literal["blocklist", "selfblock"]
#: Jail name used by the blocklist import service.
BLOCKLIST_JAIL: str = "blocklist-import"
def _derive_origin(jail: str) -> BanOrigin:
"""Derive the ban origin from the jail name.
Args:
jail: The jail that issued the ban.
Returns:
``"blocklist"`` when the jail is the dedicated blocklist-import
jail, ``"selfblock"`` otherwise.
"""
return "blocklist" if jail == BLOCKLIST_JAIL else "selfblock"
class Ban(BaseModel):
"""Domain model representing a single active or historical ban record."""
@@ -65,6 +85,10 @@ class Ban(BaseModel):
default=None,
description="ISO 3166-1 alpha-2 country code resolved from the IP.",
)
origin: BanOrigin = Field(
...,
description="Whether this ban came from a blocklist import or fail2ban itself.",
)
class BanResponse(BaseModel):
@@ -146,6 +170,10 @@ class DashboardBanItem(BaseModel):
description="Organisation name associated with the IP.",
)
ban_count: int = Field(..., ge=1, description="How many times this IP was banned.")
origin: BanOrigin = Field(
...,
description="Whether this ban came from a blocklist import or fail2ban itself.",
)
class DashboardBanListResponse(BaseModel):

View File

@@ -169,3 +169,36 @@ class LogPreviewResponse(BaseModel):
total_lines: int = Field(..., ge=0)
matched_count: int = Field(..., ge=0)
regex_error: str | None = Field(default=None, description="Set if the regex failed to compile.")
# ---------------------------------------------------------------------------
# Map color threshold models
# ---------------------------------------------------------------------------
class MapColorThresholdsResponse(BaseModel):
"""Response for ``GET /api/config/map-thresholds``."""
model_config = ConfigDict(strict=True)
threshold_high: int = Field(
..., description="Ban count for red coloring."
)
threshold_medium: int = Field(
..., description="Ban count for yellow coloring."
)
threshold_low: int = Field(
..., description="Ban count for green coloring."
)
class MapColorThresholdsUpdate(BaseModel):
"""Payload for ``PUT /api/config/map-thresholds``."""
model_config = ConfigDict(strict=True)
threshold_high: int = Field(..., gt=0, description="Ban count for red.")
threshold_medium: int = Field(
..., gt=0, description="Ban count for yellow."
)
threshold_low: int = Field(..., gt=0, description="Ban count for green.")