No canonical snake_case/camelCase serialization policy
This commit is contained in:
@@ -465,18 +465,14 @@ class BansByCountryResponse(BaseModel):
|
|||||||
|
|
||||||
## 5. Pydantic Models
|
## 5. Pydantic Models
|
||||||
|
|
||||||
- Every model inherits from `pydantic.BaseModel`.
|
### Base Class
|
||||||
- Use `model_config = ConfigDict(strict=True)` where appropriate.
|
|
||||||
- Field names use **snake_case** in Python, export as **camelCase** to the frontend via alias generators if needed.
|
Every model in `app/models/` **must** inherit from `BanGuiBaseModel` (defined in `app/models/response.py`), not from `pydantic.BaseModel` directly.
|
||||||
- Validate at the boundary — once data enters a Pydantic model it is trusted.
|
|
||||||
- Use `Field(...)` with descriptions for every field to keep auto-generated docs useful.
|
|
||||||
- Separate **request models**, **response models**, and **domain (internal) models** — do not reuse one model for all three.
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pydantic import BaseModel, Field
|
from app.models.response import BanGuiBaseModel
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
class BanResponse(BaseModel):
|
class BanResponse(BanGuiBaseModel):
|
||||||
ip: str = Field(..., description="Banned IP address")
|
ip: str = Field(..., description="Banned IP address")
|
||||||
jail: str = Field(..., description="Jail that issued the ban")
|
jail: str = Field(..., description="Jail that issued the ban")
|
||||||
banned_at: datetime = Field(..., description="UTC timestamp of the ban")
|
banned_at: datetime = Field(..., description="UTC timestamp of the ban")
|
||||||
@@ -484,6 +480,24 @@ class BanResponse(BaseModel):
|
|||||||
ban_count: int = Field(..., ge=1, description="Number of times this IP was banned")
|
ban_count: int = Field(..., ge=1, description="Number of times this IP was banned")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`BanGuiBaseModel` sets `strict=True` and documents the naming policy. Do **not** override `model_config` on individual models unless you have a specific, documented reason.
|
||||||
|
|
||||||
|
### API Field Naming Policy — snake_case everywhere
|
||||||
|
|
||||||
|
All API field names use **`snake_case`** in Python, in the JSON wire format, and in the corresponding TypeScript interfaces. There is no `alias_generator` that converts to camelCase.
|
||||||
|
|
||||||
|
- ✅ Python field: `active_jails` → JSON key: `"active_jails"` → TypeScript property: `active_jails`
|
||||||
|
- ❌ Do **not** add a camelCase `alias_generator` to individual models.
|
||||||
|
- ❌ Do **not** mix field name conventions within a single API response.
|
||||||
|
|
||||||
|
This policy eliminates a whole class of frontend–backend contract bugs. If the naming policy ever needs to change (e.g. to emit camelCase), change `BanGuiBaseModel` once — all models update automatically.
|
||||||
|
|
||||||
|
### Other Model Rules
|
||||||
|
|
||||||
|
- Validate at the boundary — once data enters a Pydantic model it is trusted.
|
||||||
|
- Use `Field(...)` with descriptions for every field to keep auto-generated docs useful.
|
||||||
|
- Separate **request models**, **response models**, and **domain (internal) models** — do not reuse one model for all three.
|
||||||
|
|
||||||
### Using `Literal` Types for Constrained Strings
|
### Using `Literal` Types for Constrained Strings
|
||||||
|
|
||||||
When a field should only accept a small set of predefined values, use `Literal` to enforce this at the type level:
|
When a field should only accept a small set of predefined values, use `Literal` to enforce this at the type level:
|
||||||
|
|||||||
@@ -1,24 +1,3 @@
|
|||||||
## 24) API response wrapper shape is inconsistent
|
|
||||||
- Where found:
|
|
||||||
- [backend/app/routers/dashboard.py](backend/app/routers/dashboard.py)
|
|
||||||
- [backend/app/routers/jails.py](backend/app/routers/jails.py)
|
|
||||||
- [frontend/src/types](frontend/src/types)
|
|
||||||
- Why this is needed:
|
|
||||||
- Inconsistent payload envelopes increase frontend branching and integration bugs.
|
|
||||||
- Goal:
|
|
||||||
- Define and enforce a consistent response envelope policy.
|
|
||||||
- What to do:
|
|
||||||
- Standardize endpoint response forms.
|
|
||||||
- Align frontend typing and parsing strategy.
|
|
||||||
- Possible traps and issues:
|
|
||||||
- Breaking contract for existing clients.
|
|
||||||
- Docs changes needed:
|
|
||||||
- Add API response style guide.
|
|
||||||
- Doc references:
|
|
||||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 25) No canonical snake_case/camelCase serialization policy
|
## 25) No canonical snake_case/camelCase serialization policy
|
||||||
- Where found:
|
- Where found:
|
||||||
- [backend/app/models/server.py](backend/app/models/server.py)
|
- [backend/app/models/server.py](backend/app/models/server.py)
|
||||||
|
|||||||
@@ -966,6 +966,15 @@ This pattern prevents **stale session flicker** — the brief moment when a user
|
|||||||
| Directories | lowercase kebab‑case or camelCase | `components/`, `hooks/` |
|
| Directories | lowercase kebab‑case or camelCase | `components/`, `hooks/` |
|
||||||
| Boolean props/variables | `is`/`has`/`should` prefix | `isLoading`, `hasError` |
|
| Boolean props/variables | `is`/`has`/`should` prefix | `isLoading`, `hasError` |
|
||||||
|
|
||||||
|
### API Field Names — snake_case
|
||||||
|
|
||||||
|
All TypeScript interface properties that mirror backend API responses use **`snake_case`**, not `camelCase`. This matches the JSON wire format emitted by the backend without any transformation layer.
|
||||||
|
|
||||||
|
- ✅ `active_jails`, `total_bans`, `log_level`, `db_purge_age`
|
||||||
|
- ❌ `activeJails`, `totalBans`, `logLevel`, `dbPurgeAge` (do **not** use camelCase for API shapes)
|
||||||
|
|
||||||
|
This applies to all interfaces in `src/types/`. Internal component state, props, and hook return values use `camelCase` as normal TypeScript convention.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Linting & Formatting
|
## 9. Linting & Formatting
|
||||||
|
|||||||
@@ -3,22 +3,20 @@
|
|||||||
Request, response, and domain models used by the auth router and service.
|
Request, response, and domain models used by the auth router and service.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BanGuiBaseModel):
|
||||||
"""Payload for ``POST /api/auth/login``."""
|
"""Payload for ``POST /api/auth/login``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
password: str = Field(
|
password: str = Field(
|
||||||
...,
|
...,
|
||||||
max_length=72,
|
max_length=72,
|
||||||
description="Master password to authenticate with (max 72 bytes due to bcrypt truncation).",
|
description="Master password to authenticate with (max 72 bytes due to bcrypt truncation).",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class LoginResponse(BanGuiBaseModel):
|
||||||
class LoginResponse(BaseModel):
|
|
||||||
"""Successful login response.
|
"""Successful login response.
|
||||||
|
|
||||||
The session token is set as an ``HttpOnly`` ``SameSite=Lax`` cookie by the
|
The session token is set as an ``HttpOnly`` ``SameSite=Lax`` cookie by the
|
||||||
@@ -30,24 +28,16 @@ class LoginResponse(BaseModel):
|
|||||||
use ``POST /api/auth/token`` instead, which does not set a cookie.
|
use ``POST /api/auth/token`` instead, which does not set a cookie.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
expires_at: str = Field(..., description="ISO 8601 UTC expiry timestamp.")
|
expires_at: str = Field(..., description="ISO 8601 UTC expiry timestamp.")
|
||||||
|
|
||||||
|
class LogoutResponse(BanGuiBaseModel):
|
||||||
class LogoutResponse(BaseModel):
|
|
||||||
"""Response body for ``POST /api/auth/logout``."""
|
"""Response body for ``POST /api/auth/logout``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
message: str = Field(default="Logged out successfully.")
|
message: str = Field(default="Logged out successfully.")
|
||||||
|
|
||||||
|
class Session(BanGuiBaseModel):
|
||||||
class Session(BaseModel):
|
|
||||||
"""Internal domain model representing a persisted session record."""
|
"""Internal domain model representing a persisted session record."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
id: int = Field(..., description="Auto-incremented row ID.")
|
id: int = Field(..., description="Auto-incremented row ID.")
|
||||||
token: str = Field(..., description="Opaque session token.")
|
token: str = Field(..., description="Opaque session token.")
|
||||||
created_at: str = Field(..., description="ISO 8601 UTC creation timestamp.")
|
created_at: str = Field(..., description="ISO 8601 UTC creation timestamp.")
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ Request, response, and domain models used by the ban router and service.
|
|||||||
import math
|
import math
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel, CollectionResponse, PaginatedListResponse
|
||||||
|
|
||||||
from app.models.response import CollectionResponse, PaginatedListResponse
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Time-range selector
|
# Time-range selector
|
||||||
@@ -25,21 +26,15 @@ TIME_RANGE_SECONDS: dict[str, int] = {
|
|||||||
"365d": 365 * 24 * 3600,
|
"365d": 365 * 24 * 3600,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BanRequest(BanGuiBaseModel):
|
||||||
class BanRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/bans`` (ban an IP)."""
|
"""Payload for ``POST /api/bans`` (ban an IP)."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="IP address to ban.")
|
ip: str = Field(..., description="IP address to ban.")
|
||||||
jail: str = Field(..., description="Jail in which to apply the ban.")
|
jail: str = Field(..., description="Jail in which to apply the ban.")
|
||||||
|
|
||||||
|
class UnbanRequest(BanGuiBaseModel):
|
||||||
class UnbanRequest(BaseModel):
|
|
||||||
"""Payload for ``DELETE /api/bans`` (unban an IP)."""
|
"""Payload for ``DELETE /api/bans`` (unban an IP)."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="IP address to unban.")
|
ip: str = Field(..., description="IP address to unban.")
|
||||||
jail: str | None = Field(
|
jail: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
@@ -50,14 +45,12 @@ class UnbanRequest(BaseModel):
|
|||||||
description="When ``true`` the IP is unbanned from every jail.",
|
description="When ``true`` the IP is unbanned from every jail.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#: Discriminator literal for the origin of a ban.
|
#: Discriminator literal for the origin of a ban.
|
||||||
BanOrigin = Literal["blocklist", "selfblock"]
|
BanOrigin = Literal["blocklist", "selfblock"]
|
||||||
|
|
||||||
#: Jail name used by the blocklist import service.
|
#: Jail name used by the blocklist import service.
|
||||||
BLOCKLIST_JAIL: str = "blocklist-import"
|
BLOCKLIST_JAIL: str = "blocklist-import"
|
||||||
|
|
||||||
|
|
||||||
def _derive_origin(jail: str) -> BanOrigin:
|
def _derive_origin(jail: str) -> BanOrigin:
|
||||||
"""Derive the ban origin from the jail name.
|
"""Derive the ban origin from the jail name.
|
||||||
|
|
||||||
@@ -70,12 +63,9 @@ def _derive_origin(jail: str) -> BanOrigin:
|
|||||||
"""
|
"""
|
||||||
return "blocklist" if jail == BLOCKLIST_JAIL else "selfblock"
|
return "blocklist" if jail == BLOCKLIST_JAIL else "selfblock"
|
||||||
|
|
||||||
|
class Ban(BanGuiBaseModel):
|
||||||
class Ban(BaseModel):
|
|
||||||
"""Domain model representing a single active or historical ban record."""
|
"""Domain model representing a single active or historical ban record."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="Banned IP address.")
|
ip: str = Field(..., description="Banned IP address.")
|
||||||
jail: str = Field(..., description="Jail that issued the ban.")
|
jail: str = Field(..., description="Jail that issued the ban.")
|
||||||
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
||||||
@@ -93,15 +83,11 @@ class Ban(BaseModel):
|
|||||||
description="Whether this ban came from a blocklist import or fail2ban itself.",
|
description="Whether this ban came from a blocklist import or fail2ban itself.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class BanResponse(BanGuiBaseModel):
|
||||||
class BanResponse(BaseModel):
|
|
||||||
"""Response containing a single ban record."""
|
"""Response containing a single ban record."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ban: Ban
|
ban: Ban
|
||||||
|
|
||||||
|
|
||||||
class BanListResponse(PaginatedListResponse[Ban]):
|
class BanListResponse(PaginatedListResponse[Ban]):
|
||||||
"""Paginated list of ban records.
|
"""Paginated list of ban records.
|
||||||
|
|
||||||
@@ -114,12 +100,9 @@ class BanListResponse(PaginatedListResponse[Ban]):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class ActiveBan(BanGuiBaseModel):
|
||||||
class ActiveBan(BaseModel):
|
|
||||||
"""A currently active ban entry returned by ``GET /api/bans/active``."""
|
"""A currently active ban entry returned by ``GET /api/bans/active``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="Banned IP address.")
|
ip: str = Field(..., description="Banned IP address.")
|
||||||
jail: str = Field(..., description="Jail holding the ban.")
|
jail: str = Field(..., description="Jail holding the ban.")
|
||||||
banned_at: str | None = Field(default=None, description="ISO 8601 UTC start of the ban.")
|
banned_at: str | None = Field(default=None, description="ISO 8601 UTC start of the ban.")
|
||||||
@@ -130,7 +113,6 @@ class ActiveBan(BaseModel):
|
|||||||
ban_count: int = Field(default=1, ge=1, description="Running ban count for this IP.")
|
ban_count: int = Field(default=1, ge=1, description="Running ban count for this IP.")
|
||||||
country: str | None = Field(default=None, description="ISO 3166-1 alpha-2 country code.")
|
country: str | None = Field(default=None, description="ISO 3166-1 alpha-2 country code.")
|
||||||
|
|
||||||
|
|
||||||
class ActiveBanListResponse(CollectionResponse[ActiveBan]):
|
class ActiveBanListResponse(CollectionResponse[ActiveBan]):
|
||||||
"""List of all currently active bans across all jails.
|
"""List of all currently active bans across all jails.
|
||||||
|
|
||||||
@@ -143,29 +125,22 @@ class ActiveBanListResponse(CollectionResponse[ActiveBan]):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class UnbanAllResponse(BanGuiBaseModel):
|
||||||
class UnbanAllResponse(BaseModel):
|
|
||||||
"""Response for ``DELETE /api/bans/all``."""
|
"""Response for ``DELETE /api/bans/all``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
message: str = Field(..., description="Human-readable summary of the operation.")
|
message: str = Field(..., description="Human-readable summary of the operation.")
|
||||||
count: int = Field(..., ge=0, description="Number of IPs that were unbanned.")
|
count: int = Field(..., ge=0, description="Number of IPs that were unbanned.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Dashboard ban-list view models
|
# Dashboard ban-list view models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class DashboardBanItem(BanGuiBaseModel):
|
||||||
class DashboardBanItem(BaseModel):
|
|
||||||
"""A single row in the dashboard ban-list table.
|
"""A single row in the dashboard ban-list table.
|
||||||
|
|
||||||
Populated from the fail2ban database and enriched with geo data.
|
Populated from the fail2ban database and enriched with geo data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="Banned IP address.")
|
ip: str = Field(..., description="Banned IP address.")
|
||||||
jail: str = Field(..., description="Jail that issued the ban.")
|
jail: str = Field(..., description="Jail that issued the ban.")
|
||||||
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
||||||
@@ -195,7 +170,6 @@ class DashboardBanItem(BaseModel):
|
|||||||
description="Whether this ban came from a blocklist import or fail2ban itself.",
|
description="Whether this ban came from a blocklist import or fail2ban itself.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DashboardBanListResponse(PaginatedListResponse[DashboardBanItem]):
|
class DashboardBanListResponse(PaginatedListResponse[DashboardBanItem]):
|
||||||
"""Paginated dashboard ban-list response.
|
"""Paginated dashboard ban-list response.
|
||||||
|
|
||||||
@@ -205,8 +179,7 @@ class DashboardBanListResponse(PaginatedListResponse[DashboardBanItem]):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class BansByCountryResponse(BanGuiBaseModel):
|
||||||
class BansByCountryResponse(BaseModel):
|
|
||||||
"""Response for the bans-by-country aggregation endpoint.
|
"""Response for the bans-by-country aggregation endpoint.
|
||||||
|
|
||||||
Contains a per-country ban count, a human-readable country name map, and
|
Contains a per-country ban count, a human-readable country name map, and
|
||||||
@@ -215,8 +188,6 @@ class BansByCountryResponse(BaseModel):
|
|||||||
single request.
|
single request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
countries: dict[str, int] = Field(
|
countries: dict[str, int] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="ISO 3166-1 alpha-2 country code → ban count.",
|
description="ISO 3166-1 alpha-2 country code → ban count.",
|
||||||
@@ -231,7 +202,6 @@ class BansByCountryResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
total: int = Field(..., ge=0, description="Total ban count in the window.")
|
total: int = Field(..., ge=0, description="Total ban count in the window.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Trend endpoint models
|
# Trend endpoint models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -252,7 +222,6 @@ BUCKET_SIZE_LABEL: dict[str, str] = {
|
|||||||
"365d": "7d",
|
"365d": "7d",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def bucket_count(range_: TimeRange) -> int:
|
def bucket_count(range_: TimeRange) -> int:
|
||||||
"""Return the number of buckets needed to cover *range_* completely.
|
"""Return the number of buckets needed to cover *range_* completely.
|
||||||
|
|
||||||
@@ -266,21 +235,15 @@ def bucket_count(range_: TimeRange) -> int:
|
|||||||
"""
|
"""
|
||||||
return math.ceil(TIME_RANGE_SECONDS[range_] / BUCKET_SECONDS[range_])
|
return math.ceil(TIME_RANGE_SECONDS[range_] / BUCKET_SECONDS[range_])
|
||||||
|
|
||||||
|
class BanTrendBucket(BanGuiBaseModel):
|
||||||
class BanTrendBucket(BaseModel):
|
|
||||||
"""A single time bucket in the ban trend series."""
|
"""A single time bucket in the ban trend series."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
timestamp: str = Field(..., description="ISO 8601 UTC start of the bucket.")
|
timestamp: str = Field(..., description="ISO 8601 UTC start of the bucket.")
|
||||||
count: int = Field(..., ge=0, description="Number of bans that started in this bucket.")
|
count: int = Field(..., ge=0, description="Number of bans that started in this bucket.")
|
||||||
|
|
||||||
|
class BanTrendResponse(BanGuiBaseModel):
|
||||||
class BanTrendResponse(BaseModel):
|
|
||||||
"""Response for the ``GET /api/dashboard/bans/trend`` endpoint."""
|
"""Response for the ``GET /api/dashboard/bans/trend`` endpoint."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
buckets: list[BanTrendBucket] = Field(
|
buckets: list[BanTrendBucket] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Time-ordered list of ban-count buckets covering the full window.",
|
description="Time-ordered list of ban-count buckets covering the full window.",
|
||||||
@@ -290,38 +253,29 @@ class BanTrendResponse(BaseModel):
|
|||||||
description="Human-readable bucket size label (e.g. '1h', '6h', '1d', '7d').",
|
description="Human-readable bucket size label (e.g. '1h', '6h', '1d', '7d').",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# By-jail endpoint models
|
# By-jail endpoint models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class JailBanCount(BanGuiBaseModel):
|
||||||
class JailBanCount(BaseModel):
|
|
||||||
"""A single jail entry in the bans-by-jail aggregation."""
|
"""A single jail entry in the bans-by-jail aggregation."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jail: str = Field(..., description="Jail name.")
|
jail: str = Field(..., description="Jail name.")
|
||||||
count: int = Field(..., ge=0, description="Number of bans recorded in this jail.")
|
count: int = Field(..., ge=0, description="Number of bans recorded in this jail.")
|
||||||
|
|
||||||
|
class BansByJailResponse(BanGuiBaseModel):
|
||||||
class BansByJailResponse(BaseModel):
|
|
||||||
"""Response for the ``GET /api/dashboard/bans/by-jail`` endpoint."""
|
"""Response for the ``GET /api/dashboard/bans/by-jail`` endpoint."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jails: list[JailBanCount] = Field(
|
jails: list[JailBanCount] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Jails ordered by ban count descending.",
|
description="Jails ordered by ban count descending.",
|
||||||
)
|
)
|
||||||
total: int = Field(..., ge=0, description="Total ban count in the selected window.")
|
total: int = Field(..., ge=0, description="Total ban count in the selected window.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Jail-specific paginated bans
|
# Jail-specific paginated bans
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class JailBannedIpsResponse(PaginatedListResponse[ActiveBan]):
|
class JailBannedIpsResponse(PaginatedListResponse[ActiveBan]):
|
||||||
"""Paginated response for ``GET /api/jails/{name}/banned``.
|
"""Paginated response for ``GET /api/jails/{name}/banned``.
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field
|
from pydantic import AnyHttpUrl, Field
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Blocklist source
|
# Blocklist source
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class BlocklistSource(BanGuiBaseModel):
|
||||||
class BlocklistSource(BaseModel):
|
|
||||||
"""Domain model for a blocklist source definition."""
|
"""Domain model for a blocklist source definition."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
url: str
|
url: str
|
||||||
@@ -27,8 +27,7 @@ class BlocklistSource(BaseModel):
|
|||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
|
||||||
|
class BlocklistSourceCreate(BanGuiBaseModel):
|
||||||
class BlocklistSourceCreate(BaseModel):
|
|
||||||
"""Payload for ``POST /api/blocklists``.
|
"""Payload for ``POST /api/blocklists``.
|
||||||
|
|
||||||
URL must use http/https scheme. The hostname must resolve to a public IP
|
URL must use http/https scheme. The hostname must resolve to a public IP
|
||||||
@@ -36,44 +35,32 @@ class BlocklistSourceCreate(BaseModel):
|
|||||||
asynchronously in the service layer.
|
asynchronously in the service layer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., min_length=1, max_length=100, description="Human-readable source name.")
|
name: str = Field(..., min_length=1, max_length=100, description="Human-readable source name.")
|
||||||
url: AnyHttpUrl = Field(..., description="URL of the blocklist file (http/https only).")
|
url: AnyHttpUrl = Field(..., description="URL of the blocklist file (http/https only).")
|
||||||
enabled: bool = Field(default=True)
|
enabled: bool = Field(default=True)
|
||||||
|
|
||||||
|
class BlocklistSourceUpdate(BanGuiBaseModel):
|
||||||
class BlocklistSourceUpdate(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/blocklists/{id}``. All fields are optional.
|
"""Payload for ``PUT /api/blocklists/{id}``. All fields are optional.
|
||||||
|
|
||||||
If URL is provided, it must use http/https scheme.
|
If URL is provided, it must use http/https scheme.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str | None = Field(default=None, min_length=1, max_length=100)
|
name: str | None = Field(default=None, min_length=1, max_length=100)
|
||||||
url: AnyHttpUrl | None = Field(default=None)
|
url: AnyHttpUrl | None = Field(default=None)
|
||||||
enabled: bool | None = Field(default=None)
|
enabled: bool | None = Field(default=None)
|
||||||
|
|
||||||
|
class BlocklistListResponse(BanGuiBaseModel):
|
||||||
class BlocklistListResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/blocklists``."""
|
"""Response for ``GET /api/blocklists``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
sources: list[BlocklistSource] = Field(default_factory=list)
|
sources: list[BlocklistSource] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Import log
|
# Import log
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ImportLogEntry(BanGuiBaseModel):
|
||||||
class ImportLogEntry(BaseModel):
|
|
||||||
"""A single blocklist import run record."""
|
"""A single blocklist import run record."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
source_id: int | None
|
source_id: int | None
|
||||||
source_url: str
|
source_url: str
|
||||||
@@ -82,24 +69,19 @@ class ImportLogEntry(BaseModel):
|
|||||||
ips_skipped: int
|
ips_skipped: int
|
||||||
errors: str | None
|
errors: str | None
|
||||||
|
|
||||||
|
class ImportLogListResponse(BanGuiBaseModel):
|
||||||
class ImportLogListResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/blocklists/log``."""
|
"""Response for ``GET /api/blocklists/log``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
items: list[ImportLogEntry] = Field(default_factory=list)
|
items: list[ImportLogEntry] = Field(default_factory=list)
|
||||||
total: int = Field(..., ge=0)
|
total: int = Field(..., ge=0)
|
||||||
page: int = Field(default=1, ge=1)
|
page: int = Field(default=1, ge=1)
|
||||||
page_size: int = Field(default=50, ge=1)
|
page_size: int = Field(default=50, ge=1)
|
||||||
total_pages: int = Field(default=1, ge=1)
|
total_pages: int = Field(default=1, ge=1)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Schedule
|
# Schedule
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class ScheduleFrequency(StrEnum):
|
class ScheduleFrequency(StrEnum):
|
||||||
"""Available import schedule frequency presets."""
|
"""Available import schedule frequency presets."""
|
||||||
|
|
||||||
@@ -107,8 +89,7 @@ class ScheduleFrequency(StrEnum):
|
|||||||
daily = "daily"
|
daily = "daily"
|
||||||
weekly = "weekly"
|
weekly = "weekly"
|
||||||
|
|
||||||
|
class ScheduleConfig(BanGuiBaseModel):
|
||||||
class ScheduleConfig(BaseModel):
|
|
||||||
"""Import schedule configuration.
|
"""Import schedule configuration.
|
||||||
|
|
||||||
The interpretation of fields depends on *frequency*:
|
The interpretation of fields depends on *frequency*:
|
||||||
@@ -132,57 +113,43 @@ class ScheduleConfig(BaseModel):
|
|||||||
description="Day of week for weekly runs (0=Monday … 6=Sunday)",
|
description="Day of week for weekly runs (0=Monday … 6=Sunday)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ScheduleInfo(BanGuiBaseModel):
|
||||||
class ScheduleInfo(BaseModel):
|
|
||||||
"""Current schedule configuration together with runtime metadata."""
|
"""Current schedule configuration together with runtime metadata."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
config: ScheduleConfig
|
config: ScheduleConfig
|
||||||
next_run_at: str | None
|
next_run_at: str | None
|
||||||
last_run_at: str | None
|
last_run_at: str | None
|
||||||
last_run_errors: bool | None = None
|
last_run_errors: bool | None = None
|
||||||
"""``True`` if the most recent import had errors, ``False`` if clean, ``None`` if never run."""
|
"""``True`` if the most recent import had errors, ``False`` if clean, ``None`` if never run."""
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Import results
|
# Import results
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ImportSourceResult(BanGuiBaseModel):
|
||||||
class ImportSourceResult(BaseModel):
|
|
||||||
"""Result of importing a single blocklist source."""
|
"""Result of importing a single blocklist source."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
source_id: int | None
|
source_id: int | None
|
||||||
source_url: str
|
source_url: str
|
||||||
ips_imported: int
|
ips_imported: int
|
||||||
ips_skipped: int
|
ips_skipped: int
|
||||||
error: str | None
|
error: str | None
|
||||||
|
|
||||||
|
class ImportRunResult(BanGuiBaseModel):
|
||||||
class ImportRunResult(BaseModel):
|
|
||||||
"""Aggregated result from a full import run across all enabled sources."""
|
"""Aggregated result from a full import run across all enabled sources."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
results: list[ImportSourceResult] = Field(default_factory=list)
|
results: list[ImportSourceResult] = Field(default_factory=list)
|
||||||
total_imported: int
|
total_imported: int
|
||||||
total_skipped: int
|
total_skipped: int
|
||||||
errors_count: int
|
errors_count: int
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Preview
|
# Preview
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class PreviewResponse(BanGuiBaseModel):
|
||||||
class PreviewResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/blocklists/{id}/preview``."""
|
"""Response for ``GET /api/blocklists/{id}/preview``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
entries: list[str] = Field(default_factory=list, description="Sample of valid IP entries")
|
entries: list[str] = Field(default_factory=list, description="Sample of valid IP entries")
|
||||||
total_lines: int
|
total_lines: int
|
||||||
valid_count: int
|
valid_count: int
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.models.response import CollectionResponse
|
from app.models.response import BanGuiBaseModel, CollectionResponse
|
||||||
from app.utils.path_utils import validate_log_path
|
from app.utils.path_utils import validate_log_path
|
||||||
|
|
||||||
DNSMode = Literal["yes", "warn", "no", "raw"]
|
DNSMode = Literal["yes", "warn", "no", "raw"]
|
||||||
@@ -23,12 +23,9 @@ LogTarget = Literal["STDOUT", "STDERR", "SYSLOG"]
|
|||||||
# Ban-time escalation
|
# Ban-time escalation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class BantimeEscalation(BanGuiBaseModel):
|
||||||
class BantimeEscalation(BaseModel):
|
|
||||||
"""Incremental ban-time escalation configuration for a jail."""
|
"""Incremental ban-time escalation configuration for a jail."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
increment: bool = Field(
|
increment: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether incremental banning is enabled.",
|
description="Whether incremental banning is enabled.",
|
||||||
@@ -58,12 +55,9 @@ class BantimeEscalation(BaseModel):
|
|||||||
description="Count repeat offences across all jails, not just the current one.",
|
description="Count repeat offences across all jails, not just the current one.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class BantimeEscalationUpdate(BanGuiBaseModel):
|
||||||
class BantimeEscalationUpdate(BaseModel):
|
|
||||||
"""Partial update payload for ban-time escalation settings."""
|
"""Partial update payload for ban-time escalation settings."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
increment: bool | None = Field(default=None)
|
increment: bool | None = Field(default=None)
|
||||||
factor: float | None = Field(default=None)
|
factor: float | None = Field(default=None)
|
||||||
formula: str | None = Field(default=None)
|
formula: str | None = Field(default=None)
|
||||||
@@ -72,17 +66,13 @@ class BantimeEscalationUpdate(BaseModel):
|
|||||||
rnd_time: int | None = Field(default=None)
|
rnd_time: int | None = Field(default=None)
|
||||||
overall_jails: bool | None = Field(default=None)
|
overall_jails: bool | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Jail configuration models
|
# Jail configuration models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class JailConfig(BanGuiBaseModel):
|
||||||
class JailConfig(BaseModel):
|
|
||||||
"""Configuration snapshot of a single jail (editable fields)."""
|
"""Configuration snapshot of a single jail (editable fields)."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Jail name as configured in fail2ban.")
|
name: str = Field(..., description="Jail name as configured in fail2ban.")
|
||||||
ban_time: int = Field(..., description="Ban duration in seconds. -1 for permanent.")
|
ban_time: int = Field(..., description="Ban duration in seconds. -1 for permanent.")
|
||||||
max_retry: int = Field(..., ge=1, description="Number of failures before a ban is issued.")
|
max_retry: int = Field(..., ge=1, description="Number of failures before a ban is issued.")
|
||||||
@@ -101,15 +91,11 @@ class JailConfig(BaseModel):
|
|||||||
description="Incremental ban-time escalation settings, or None if not configured.",
|
description="Incremental ban-time escalation settings, or None if not configured.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class JailConfigResponse(BanGuiBaseModel):
|
||||||
class JailConfigResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/jails/{name}``."""
|
"""Response for ``GET /api/config/jails/{name}``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jail: JailConfig
|
jail: JailConfig
|
||||||
|
|
||||||
|
|
||||||
class JailConfigListResponse(CollectionResponse[JailConfig]):
|
class JailConfigListResponse(CollectionResponse[JailConfig]):
|
||||||
"""Response for ``GET /api/config/jails``.
|
"""Response for ``GET /api/config/jails``.
|
||||||
|
|
||||||
@@ -118,12 +104,9 @@ class JailConfigListResponse(CollectionResponse[JailConfig]):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class JailConfigUpdate(BanGuiBaseModel):
|
||||||
class JailConfigUpdate(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/config/jails/{name}``."""
|
"""Payload for ``PUT /api/config/jails/{name}``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ban_time: int | None = Field(default=None, description="Ban duration in seconds. -1 for permanent.")
|
ban_time: int | None = Field(default=None, description="Ban duration in seconds. -1 for permanent.")
|
||||||
max_retry: int | None = Field(default=None, ge=1)
|
max_retry: int | None = Field(default=None, ge=1)
|
||||||
find_time: int | None = Field(default=None, ge=1)
|
find_time: int | None = Field(default=None, ge=1)
|
||||||
@@ -140,26 +123,19 @@ class JailConfigUpdate(BaseModel):
|
|||||||
description="Incremental ban-time escalation settings to update.",
|
description="Incremental ban-time escalation settings to update.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Regex tester models
|
# Regex tester models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class RegexTestRequest(BanGuiBaseModel):
|
||||||
class RegexTestRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/regex-test``."""
|
"""Payload for ``POST /api/config/regex-test``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_line: str = Field(..., description="Sample log line to test against.")
|
log_line: str = Field(..., description="Sample log line to test against.")
|
||||||
fail_regex: str = Field(..., description="Regex pattern to match.")
|
fail_regex: str = Field(..., description="Regex pattern to match.")
|
||||||
|
|
||||||
|
class RegexTestResponse(BanGuiBaseModel):
|
||||||
class RegexTestResponse(BaseModel):
|
|
||||||
"""Result of a regex test."""
|
"""Result of a regex test."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
matched: bool = Field(..., description="Whether the pattern matched the log line.")
|
matched: bool = Field(..., description="Whether the pattern matched the log line.")
|
||||||
groups: list[str] = Field(
|
groups: list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
@@ -170,17 +146,13 @@ class RegexTestResponse(BaseModel):
|
|||||||
description="Compilation error message if the regex is invalid.",
|
description="Compilation error message if the regex is invalid.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Global config models
|
# Global config models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class GlobalConfigResponse(BanGuiBaseModel):
|
||||||
class GlobalConfigResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/global``."""
|
"""Response for ``GET /api/config/global``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_level: LogLevel
|
log_level: LogLevel
|
||||||
log_target: str = Field(..., description="Log target: STDOUT, STDERR, SYSLOG, or a validated file path.")
|
log_target: str = Field(..., description="Log target: STDOUT, STDERR, SYSLOG, or a validated file path.")
|
||||||
db_purge_age: int = Field(..., description="Seconds after which ban records are purged from the fail2ban DB.")
|
db_purge_age: int = Field(..., description="Seconds after which ban records are purged from the fail2ban DB.")
|
||||||
@@ -222,12 +194,9 @@ class GlobalConfigResponse(BaseModel):
|
|||||||
f"Log target {value!r} is outside allowed directories: {allowed_dirs_str}"
|
f"Log target {value!r} is outside allowed directories: {allowed_dirs_str}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class GlobalConfigUpdate(BanGuiBaseModel):
|
||||||
class GlobalConfigUpdate(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/config/global``."""
|
"""Payload for ``PUT /api/config/global``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_level: LogLevel | None = Field(
|
log_level: LogLevel | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Log level: CRITICAL, ERROR, WARNING, NOTICE, INFO, or DEBUG.",
|
description="Log level: CRITICAL, ERROR, WARNING, NOTICE, INFO, or DEBUG.",
|
||||||
@@ -278,17 +247,13 @@ class GlobalConfigUpdate(BaseModel):
|
|||||||
f"Log target {value!r} is outside allowed directories: {allowed_dirs_str}"
|
f"Log target {value!r} is outside allowed directories: {allowed_dirs_str}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Log observation / preview models
|
# Log observation / preview models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class AddLogPathRequest(BanGuiBaseModel):
|
||||||
class AddLogPathRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/jails/{name}/logpath``."""
|
"""Payload for ``POST /api/config/jails/{name}/logpath``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_path: str = Field(..., description="Absolute path to the log file to monitor.")
|
log_path: str = Field(..., description="Absolute path to the log file to monitor.")
|
||||||
tail: bool = Field(
|
tail: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
@@ -311,49 +276,35 @@ class AddLogPathRequest(BaseModel):
|
|||||||
"""
|
"""
|
||||||
return validate_log_path(value)
|
return validate_log_path(value)
|
||||||
|
|
||||||
|
class LogPreviewRequest(BanGuiBaseModel):
|
||||||
|
|
||||||
class LogPreviewRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/preview-log``."""
|
"""Payload for ``POST /api/config/preview-log``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_path: str = Field(..., description="Absolute path to the log file to preview.")
|
log_path: str = Field(..., description="Absolute path to the log file to preview.")
|
||||||
fail_regex: str = Field(..., description="Regex pattern to test against log lines.")
|
fail_regex: str = Field(..., description="Regex pattern to test against log lines.")
|
||||||
num_lines: int = Field(default=200, ge=1, le=5000, description="Number of lines to read from the end of the file.")
|
num_lines: int = Field(default=200, ge=1, le=5000, description="Number of lines to read from the end of the file.")
|
||||||
|
|
||||||
|
class LogPreviewLine(BanGuiBaseModel):
|
||||||
class LogPreviewLine(BaseModel):
|
|
||||||
"""A single log line with match information."""
|
"""A single log line with match information."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
line: str
|
line: str
|
||||||
matched: bool
|
matched: bool
|
||||||
groups: list[str] = Field(default_factory=list)
|
groups: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
class LogPreviewResponse(BanGuiBaseModel):
|
||||||
class LogPreviewResponse(BaseModel):
|
|
||||||
"""Response for ``POST /api/config/preview-log``."""
|
"""Response for ``POST /api/config/preview-log``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
lines: list[LogPreviewLine] = Field(default_factory=list)
|
lines: list[LogPreviewLine] = Field(default_factory=list)
|
||||||
total_lines: int = Field(..., ge=0)
|
total_lines: int = Field(..., ge=0)
|
||||||
matched_count: 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.")
|
regex_error: str | None = Field(default=None, description="Set if the regex failed to compile.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Map color threshold models
|
# Map color threshold models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MapColorThresholdsResponse(BanGuiBaseModel):
|
||||||
class MapColorThresholdsResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/map-thresholds``."""
|
"""Response for ``GET /api/config/map-thresholds``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
threshold_high: int = Field(
|
threshold_high: int = Field(
|
||||||
..., description="Ban count for red coloring."
|
..., description="Ban count for red coloring."
|
||||||
)
|
)
|
||||||
@@ -364,25 +315,20 @@ class MapColorThresholdsResponse(BaseModel):
|
|||||||
..., description="Ban count for green coloring."
|
..., description="Ban count for green coloring."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class MapColorThresholdsUpdate(BanGuiBaseModel):
|
||||||
class MapColorThresholdsUpdate(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/config/map-thresholds``."""
|
"""Payload for ``PUT /api/config/map-thresholds``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
threshold_high: int = Field(..., gt=0, description="Ban count for red.")
|
threshold_high: int = Field(..., gt=0, description="Ban count for red.")
|
||||||
threshold_medium: int = Field(
|
threshold_medium: int = Field(
|
||||||
..., gt=0, description="Ban count for yellow."
|
..., gt=0, description="Ban count for yellow."
|
||||||
)
|
)
|
||||||
threshold_low: int = Field(..., gt=0, description="Ban count for green.")
|
threshold_low: int = Field(..., gt=0, description="Ban count for green.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Parsed filter file models
|
# Parsed filter file models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class FilterConfig(BanGuiBaseModel):
|
||||||
class FilterConfig(BaseModel):
|
|
||||||
"""Structured representation of a ``filter.d/*.conf`` file.
|
"""Structured representation of a ``filter.d/*.conf`` file.
|
||||||
|
|
||||||
The ``active``, ``used_by_jails``, ``source_file``, and
|
The ``active``, ``used_by_jails``, ``source_file``, and
|
||||||
@@ -393,8 +339,6 @@ class FilterConfig(BaseModel):
|
|||||||
these fields carry their default values.
|
these fields carry their default values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Filter base name, e.g. ``sshd``.")
|
name: str = Field(..., description="Filter base name, e.g. ``sshd``.")
|
||||||
filename: str = Field(..., description="Actual filename, e.g. ``sshd.conf``.")
|
filename: str = Field(..., description="Actual filename, e.g. ``sshd.conf``.")
|
||||||
# [INCLUDES]
|
# [INCLUDES]
|
||||||
@@ -458,15 +402,12 @@ class FilterConfig(BaseModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FilterConfigUpdate(BanGuiBaseModel):
|
||||||
class FilterConfigUpdate(BaseModel):
|
|
||||||
"""Partial update payload for a parsed filter file.
|
"""Partial update payload for a parsed filter file.
|
||||||
|
|
||||||
Only explicitly set (non-``None``) fields are written back.
|
Only explicitly set (non-``None``) fields are written back.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
before: str | None = Field(default=None)
|
before: str | None = Field(default=None)
|
||||||
after: str | None = Field(default=None)
|
after: str | None = Field(default=None)
|
||||||
variables: dict[str, str] | None = Field(default=None)
|
variables: dict[str, str] | None = Field(default=None)
|
||||||
@@ -477,8 +418,7 @@ class FilterConfigUpdate(BaseModel):
|
|||||||
datepattern: str | None = Field(default=None)
|
datepattern: str | None = Field(default=None)
|
||||||
journalmatch: str | None = Field(default=None)
|
journalmatch: str | None = Field(default=None)
|
||||||
|
|
||||||
|
class FilterUpdateRequest(BanGuiBaseModel):
|
||||||
class FilterUpdateRequest(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/config/filters/{name}``.
|
"""Payload for ``PUT /api/config/filters/{name}``.
|
||||||
|
|
||||||
Accepts only the user-editable ``[Definition]`` fields. Fields left as
|
Accepts only the user-editable ``[Definition]`` fields. Fields left as
|
||||||
@@ -486,8 +426,6 @@ class FilterUpdateRequest(BaseModel):
|
|||||||
preserved.
|
preserved.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
failregex: list[str] | None = Field(
|
failregex: list[str] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Updated failure-detection regex patterns. ``None`` = keep existing.",
|
description="Updated failure-detection regex patterns. ``None`` = keep existing.",
|
||||||
@@ -505,15 +443,12 @@ class FilterUpdateRequest(BaseModel):
|
|||||||
description="Systemd journal match expression. ``None`` = keep existing.",
|
description="Systemd journal match expression. ``None`` = keep existing.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FilterCreateRequest(BanGuiBaseModel):
|
||||||
class FilterCreateRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/filters``.
|
"""Payload for ``POST /api/config/filters``.
|
||||||
|
|
||||||
Creates a new user-defined filter at ``filter.d/{name}.local``.
|
Creates a new user-defined filter at ``filter.d/{name}.local``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(
|
name: str = Field(
|
||||||
...,
|
...,
|
||||||
description="Filter base name (e.g. ``my-custom-filter``). Must not already exist in ``filter.d/``.",
|
description="Filter base name (e.g. ``my-custom-filter``). Must not already exist in ``filter.d/``.",
|
||||||
@@ -539,23 +474,17 @@ class FilterCreateRequest(BaseModel):
|
|||||||
description="Systemd journal match expression.",
|
description="Systemd journal match expression.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class AssignFilterRequest(BanGuiBaseModel):
|
||||||
class AssignFilterRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/jails/{jail_name}/filter``."""
|
"""Payload for ``POST /api/config/jails/{jail_name}/filter``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
filter_name: str = Field(
|
filter_name: str = Field(
|
||||||
...,
|
...,
|
||||||
description="Filter base name to assign to the jail (e.g. ``sshd``).",
|
description="Filter base name to assign to the jail (e.g. ``sshd``).",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FilterListResponse(BanGuiBaseModel):
|
||||||
class FilterListResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/filters``."""
|
"""Response for ``GET /api/config/filters``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
filters: list[FilterConfig] = Field(
|
filters: list[FilterConfig] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description=(
|
description=(
|
||||||
@@ -565,17 +494,13 @@ class FilterListResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
total: int = Field(..., ge=0, description="Total number of filters found.")
|
total: int = Field(..., ge=0, description="Total number of filters found.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Parsed action file models
|
# Parsed action file models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ActionConfig(BanGuiBaseModel):
|
||||||
class ActionConfig(BaseModel):
|
|
||||||
"""Structured representation of an ``action.d/*.conf`` file."""
|
"""Structured representation of an ``action.d/*.conf`` file."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Action base name, e.g. ``iptables``.")
|
name: str = Field(..., description="Action base name, e.g. ``iptables``.")
|
||||||
filename: str = Field(..., description="Actual filename, e.g. ``iptables.conf``.")
|
filename: str = Field(..., description="Actual filename, e.g. ``iptables.conf``.")
|
||||||
# [INCLUDES]
|
# [INCLUDES]
|
||||||
@@ -644,12 +569,9 @@ class ActionConfig(BaseModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ActionConfigUpdate(BanGuiBaseModel):
|
||||||
class ActionConfigUpdate(BaseModel):
|
|
||||||
"""Partial update payload for a parsed action file."""
|
"""Partial update payload for a parsed action file."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
before: str | None = Field(default=None)
|
before: str | None = Field(default=None)
|
||||||
after: str | None = Field(default=None)
|
after: str | None = Field(default=None)
|
||||||
actionstart: str | None = Field(default=None)
|
actionstart: str | None = Field(default=None)
|
||||||
@@ -661,12 +583,9 @@ class ActionConfigUpdate(BaseModel):
|
|||||||
definition_vars: dict[str, str] | None = Field(default=None)
|
definition_vars: dict[str, str] | None = Field(default=None)
|
||||||
init_vars: dict[str, str] | None = Field(default=None)
|
init_vars: dict[str, str] | None = Field(default=None)
|
||||||
|
|
||||||
|
class ActionListResponse(BanGuiBaseModel):
|
||||||
class ActionListResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/actions``."""
|
"""Response for ``GET /api/config/actions``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
actions: list[ActionConfig] = Field(
|
actions: list[ActionConfig] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description=(
|
description=(
|
||||||
@@ -676,16 +595,13 @@ class ActionListResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
total: int = Field(..., ge=0, description="Total number of actions found.")
|
total: int = Field(..., ge=0, description="Total number of actions found.")
|
||||||
|
|
||||||
|
class ActionUpdateRequest(BanGuiBaseModel):
|
||||||
class ActionUpdateRequest(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/config/actions/{name}``.
|
"""Payload for ``PUT /api/config/actions/{name}``.
|
||||||
|
|
||||||
Accepts only the user-editable ``[Definition]`` lifecycle fields and
|
Accepts only the user-editable ``[Definition]`` lifecycle fields and
|
||||||
``[Init]`` parameters. Fields left as ``None`` are not changed.
|
``[Init]`` parameters. Fields left as ``None`` are not changed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
actionstart: str | None = Field(
|
actionstart: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Updated ``actionstart`` command. ``None`` = keep existing.",
|
description="Updated ``actionstart`` command. ``None`` = keep existing.",
|
||||||
@@ -719,15 +635,12 @@ class ActionUpdateRequest(BaseModel):
|
|||||||
description="``[Init]`` parameters to set. ``None`` = keep existing.",
|
description="``[Init]`` parameters to set. ``None`` = keep existing.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ActionCreateRequest(BanGuiBaseModel):
|
||||||
class ActionCreateRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/actions``.
|
"""Payload for ``POST /api/config/actions``.
|
||||||
|
|
||||||
Creates a new user-defined action at ``action.d/{name}.local``.
|
Creates a new user-defined action at ``action.d/{name}.local``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(
|
name: str = Field(
|
||||||
...,
|
...,
|
||||||
description="Action base name (e.g. ``my-custom-action``). Must not already exist.",
|
description="Action base name (e.g. ``my-custom-action``). Must not already exist.",
|
||||||
@@ -747,12 +660,9 @@ class ActionCreateRequest(BaseModel):
|
|||||||
description="``[Init]`` runtime parameters.",
|
description="``[Init]`` runtime parameters.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class AssignActionRequest(BanGuiBaseModel):
|
||||||
class AssignActionRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/jails/{jail_name}/action``."""
|
"""Payload for ``POST /api/config/jails/{jail_name}/action``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
action_name: str = Field(
|
action_name: str = Field(
|
||||||
...,
|
...,
|
||||||
description="Action base name to add to the jail (e.g. ``iptables-multiport``).",
|
description="Action base name to add to the jail (e.g. ``iptables-multiport``).",
|
||||||
@@ -765,17 +675,13 @@ class AssignActionRequest(BaseModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Jail file config models (Task 6.1)
|
# Jail file config models (Task 6.1)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class JailSectionConfig(BanGuiBaseModel):
|
||||||
class JailSectionConfig(BaseModel):
|
|
||||||
"""Settings within a single [jailname] section of a jail.d file."""
|
"""Settings within a single [jailname] section of a jail.d file."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
enabled: bool | None = Field(default=None, description="Whether this jail is enabled.")
|
enabled: bool | None = Field(default=None, description="Whether this jail is enabled.")
|
||||||
port: str | None = Field(default=None, description="Port(s) to monitor (e.g. 'ssh' or '22,2222').")
|
port: str | None = Field(default=None, description="Port(s) to monitor (e.g. 'ssh' or '22,2222').")
|
||||||
filter: str | None = Field(default=None, description="Filter name to use (e.g. 'sshd').")
|
filter: str | None = Field(default=None, description="Filter name to use (e.g. 'sshd').")
|
||||||
@@ -787,36 +693,28 @@ class JailSectionConfig(BaseModel):
|
|||||||
backend: BackendType | None = Field(default=None, description="Log monitoring backend.")
|
backend: BackendType | None = Field(default=None, description="Log monitoring backend.")
|
||||||
extra: dict[str, str] = Field(default_factory=dict, description="Additional settings not captured by named fields.")
|
extra: dict[str, str] = Field(default_factory=dict, description="Additional settings not captured by named fields.")
|
||||||
|
|
||||||
|
class JailFileConfig(BanGuiBaseModel):
|
||||||
class JailFileConfig(BaseModel):
|
|
||||||
"""Structured representation of a jail.d/*.conf file."""
|
"""Structured representation of a jail.d/*.conf file."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
filename: str = Field(..., description="Filename including extension (e.g. 'sshd.conf').")
|
filename: str = Field(..., description="Filename including extension (e.g. 'sshd.conf').")
|
||||||
jails: dict[str, JailSectionConfig] = Field(
|
jails: dict[str, JailSectionConfig] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Mapping of jail name → settings for each [section] in the file.",
|
description="Mapping of jail name → settings for each [section] in the file.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class JailFileConfigUpdate(BanGuiBaseModel):
|
||||||
class JailFileConfigUpdate(BaseModel):
|
|
||||||
"""Partial update payload for a jail.d file."""
|
"""Partial update payload for a jail.d file."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jails: dict[str, JailSectionConfig] | None = Field(
|
jails: dict[str, JailSectionConfig] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Jail section updates. Only jails present in this dict are updated.",
|
description="Jail section updates. Only jails present in this dict are updated.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Inactive jail models (Stage 1)
|
# Inactive jail models (Stage 1)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class InactiveJail(BanGuiBaseModel):
|
||||||
class InactiveJail(BaseModel):
|
|
||||||
"""A jail defined in fail2ban config files that is not currently active.
|
"""A jail defined in fail2ban config files that is not currently active.
|
||||||
|
|
||||||
A jail is considered inactive when its ``enabled`` key is ``false`` (or
|
A jail is considered inactive when its ``enabled`` key is ``false`` (or
|
||||||
@@ -825,8 +723,6 @@ class InactiveJail(BaseModel):
|
|||||||
running.
|
running.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Jail name from the config section header.")
|
name: str = Field(..., description="Jail name from the config section header.")
|
||||||
filter: str = Field(
|
filter: str = Field(
|
||||||
...,
|
...,
|
||||||
@@ -920,7 +816,6 @@ class InactiveJail(BaseModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InactiveJailListResponse(CollectionResponse[InactiveJail]):
|
class InactiveJailListResponse(CollectionResponse[InactiveJail]):
|
||||||
"""Response for ``GET /api/config/jails/inactive``.
|
"""Response for ``GET /api/config/jails/inactive``.
|
||||||
|
|
||||||
@@ -929,8 +824,7 @@ class InactiveJailListResponse(CollectionResponse[InactiveJail]):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class ActivateJailRequest(BanGuiBaseModel):
|
||||||
class ActivateJailRequest(BaseModel):
|
|
||||||
"""Optional override values when activating an inactive jail.
|
"""Optional override values when activating an inactive jail.
|
||||||
|
|
||||||
All fields are optional. Omitted fields are not written to the
|
All fields are optional. Omitted fields are not written to the
|
||||||
@@ -938,8 +832,6 @@ class ActivateJailRequest(BaseModel):
|
|||||||
values.
|
values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
bantime: str | None = Field(
|
bantime: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Override ban duration, e.g. ``1h`` or ``3600``.",
|
description="Override ban duration, e.g. ``1h`` or ``3600``.",
|
||||||
@@ -962,12 +854,9 @@ class ActivateJailRequest(BaseModel):
|
|||||||
description="Override log file paths.",
|
description="Override log file paths.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class JailActivationResponse(BanGuiBaseModel):
|
||||||
class JailActivationResponse(BaseModel):
|
|
||||||
"""Response for jail activation and deactivation endpoints."""
|
"""Response for jail activation and deactivation endpoints."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Name of the affected jail.")
|
name: str = Field(..., description="Name of the affected jail.")
|
||||||
active: bool = Field(
|
active: bool = Field(
|
||||||
...,
|
...,
|
||||||
@@ -996,29 +885,22 @@ class JailActivationResponse(BaseModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Jail validation models (Task 3)
|
# Jail validation models (Task 3)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class JailValidationIssue(BanGuiBaseModel):
|
||||||
class JailValidationIssue(BaseModel):
|
|
||||||
"""A single issue found during pre-activation validation of a jail config."""
|
"""A single issue found during pre-activation validation of a jail config."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
field: str = Field(
|
field: str = Field(
|
||||||
...,
|
...,
|
||||||
description="Config field associated with this issue, e.g. 'filter', 'failregex', 'logpath'.",
|
description="Config field associated with this issue, e.g. 'filter', 'failregex', 'logpath'.",
|
||||||
)
|
)
|
||||||
message: str = Field(..., description="Human-readable description of the issue.")
|
message: str = Field(..., description="Human-readable description of the issue.")
|
||||||
|
|
||||||
|
class JailValidationResult(BanGuiBaseModel):
|
||||||
class JailValidationResult(BaseModel):
|
|
||||||
"""Result of pre-activation validation of a single jail configuration."""
|
"""Result of pre-activation validation of a single jail configuration."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jail_name: str = Field(..., description="Name of the validated jail.")
|
jail_name: str = Field(..., description="Name of the validated jail.")
|
||||||
valid: bool = Field(..., description="True when no issues were found.")
|
valid: bool = Field(..., description="True when no issues were found.")
|
||||||
issues: list[JailValidationIssue] = Field(
|
issues: list[JailValidationIssue] = Field(
|
||||||
@@ -1026,17 +908,13 @@ class JailValidationResult(BaseModel):
|
|||||||
description="Validation issues found. Empty when valid=True.",
|
description="Validation issues found. Empty when valid=True.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Rollback response model (Task 3)
|
# Rollback response model (Task 3)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class RollbackResponse(BanGuiBaseModel):
|
||||||
class RollbackResponse(BaseModel):
|
|
||||||
"""Response for ``POST /api/config/jails/{name}/rollback``."""
|
"""Response for ``POST /api/config/jails/{name}/rollback``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jail_name: str = Field(..., description="Name of the jail that was disabled.")
|
jail_name: str = Field(..., description="Name of the jail that was disabled.")
|
||||||
disabled: bool = Field(
|
disabled: bool = Field(
|
||||||
...,
|
...,
|
||||||
@@ -1053,17 +931,13 @@ class RollbackResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
message: str = Field(..., description="Human-readable result message.")
|
message: str = Field(..., description="Human-readable result message.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Pending recovery model (Task 3)
|
# Pending recovery model (Task 3)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class PendingRecovery(BanGuiBaseModel):
|
||||||
class PendingRecovery(BaseModel):
|
|
||||||
"""Records a probable activation-caused fail2ban crash pending user action."""
|
"""Records a probable activation-caused fail2ban crash pending user action."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jail_name: str = Field(
|
jail_name: str = Field(
|
||||||
...,
|
...,
|
||||||
description="Name of the jail whose activation likely caused the crash.",
|
description="Name of the jail whose activation likely caused the crash.",
|
||||||
@@ -1081,29 +955,22 @@ class PendingRecovery(BaseModel):
|
|||||||
description="Whether fail2ban has been successfully restarted.",
|
description="Whether fail2ban has been successfully restarted.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# fail2ban log viewer models
|
# fail2ban log viewer models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Fail2BanLogResponse(BanGuiBaseModel):
|
||||||
class Fail2BanLogResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/fail2ban-log``."""
|
"""Response for ``GET /api/config/fail2ban-log``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_path: str = Field(..., description="Resolved absolute path of the log file being read.")
|
log_path: str = Field(..., description="Resolved absolute path of the log file being read.")
|
||||||
lines: list[str] = Field(default_factory=list, description="Log lines returned (tail, optionally filtered).")
|
lines: list[str] = Field(default_factory=list, description="Log lines returned (tail, optionally filtered).")
|
||||||
total_lines: int = Field(..., ge=0, description="Total number of lines in the file before filtering.")
|
total_lines: int = Field(..., ge=0, description="Total number of lines in the file before filtering.")
|
||||||
log_level: str = Field(..., description="Current fail2ban log level.")
|
log_level: str = Field(..., description="Current fail2ban log level.")
|
||||||
log_target: str = Field(..., description="Current fail2ban log target (file path or special value).")
|
log_target: str = Field(..., description="Current fail2ban log target (file path or special value).")
|
||||||
|
|
||||||
|
class ServiceStatusResponse(BanGuiBaseModel):
|
||||||
class ServiceStatusResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/service-status``."""
|
"""Response for ``GET /api/config/service-status``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
online: bool = Field(..., description="Whether fail2ban is reachable via its socket.")
|
online: bool = Field(..., description="Whether fail2ban is reachable via its socket.")
|
||||||
version: str | None = Field(default=None, description="BanGUI application version (or None when offline).")
|
version: str | None = Field(default=None, description="BanGUI application version (or None when offline).")
|
||||||
jail_count: int = Field(default=0, ge=0, description="Number of currently active jails.")
|
jail_count: int = Field(default=0, ge=0, description="Number of currently active jails.")
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ Covers jail config files (``jail.d/``), filter definitions (``filter.d/``),
|
|||||||
and action definitions (``action.d/``).
|
and action definitions (``action.d/``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Jail config file models (Task 4a)
|
# Jail config file models (Task 4a)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class JailConfigFile(BanGuiBaseModel):
|
||||||
class JailConfigFile(BaseModel):
|
|
||||||
"""Metadata for a single jail configuration file in ``jail.d/``."""
|
"""Metadata for a single jail configuration file in ``jail.d/``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Jail name (file stem, e.g. ``sshd``).")
|
name: str = Field(..., description="Jail name (file stem, e.g. ``sshd``).")
|
||||||
filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).")
|
filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).")
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
@@ -26,81 +26,56 @@ class JailConfigFile(BaseModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class JailConfigFilesResponse(BanGuiBaseModel):
|
||||||
class JailConfigFilesResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/config/jail-files``."""
|
"""Response for ``GET /api/config/jail-files``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
files: list[JailConfigFile] = Field(default_factory=list)
|
files: list[JailConfigFile] = Field(default_factory=list)
|
||||||
total: int = Field(..., ge=0)
|
total: int = Field(..., ge=0)
|
||||||
|
|
||||||
|
class JailConfigFileContent(BanGuiBaseModel):
|
||||||
class JailConfigFileContent(BaseModel):
|
|
||||||
"""Single jail config file with its raw content."""
|
"""Single jail config file with its raw content."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Jail name (file stem).")
|
name: str = Field(..., description="Jail name (file stem).")
|
||||||
filename: str = Field(..., description="Actual filename.")
|
filename: str = Field(..., description="Actual filename.")
|
||||||
enabled: bool = Field(..., description="Whether the jail is enabled.")
|
enabled: bool = Field(..., description="Whether the jail is enabled.")
|
||||||
content: str = Field(..., description="Raw file content.")
|
content: str = Field(..., description="Raw file content.")
|
||||||
|
|
||||||
|
class JailConfigFileEnabledUpdate(BanGuiBaseModel):
|
||||||
class JailConfigFileEnabledUpdate(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/config/jail-files/{filename}/enabled``."""
|
"""Payload for ``PUT /api/config/jail-files/{filename}/enabled``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
enabled: bool = Field(..., description="New enabled state for this jail.")
|
enabled: bool = Field(..., description="New enabled state for this jail.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Generic conf-file entry (shared by filter.d and action.d)
|
# Generic conf-file entry (shared by filter.d and action.d)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ConfFileEntry(BanGuiBaseModel):
|
||||||
class ConfFileEntry(BaseModel):
|
|
||||||
"""Metadata for a single ``.conf`` or ``.local`` file."""
|
"""Metadata for a single ``.conf`` or ``.local`` file."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Base name without extension (e.g. ``sshd``).")
|
name: str = Field(..., description="Base name without extension (e.g. ``sshd``).")
|
||||||
filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).")
|
filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).")
|
||||||
|
|
||||||
|
class ConfFilesResponse(BanGuiBaseModel):
|
||||||
class ConfFilesResponse(BaseModel):
|
|
||||||
"""Response for list endpoints (``GET /api/config/filters`` and ``GET /api/config/actions``)."""
|
"""Response for list endpoints (``GET /api/config/filters`` and ``GET /api/config/actions``)."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
files: list[ConfFileEntry] = Field(default_factory=list)
|
files: list[ConfFileEntry] = Field(default_factory=list)
|
||||||
total: int = Field(..., ge=0)
|
total: int = Field(..., ge=0)
|
||||||
|
|
||||||
|
class ConfFileContent(BanGuiBaseModel):
|
||||||
class ConfFileContent(BaseModel):
|
|
||||||
"""A conf file with its raw text content."""
|
"""A conf file with its raw text content."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Base name without extension.")
|
name: str = Field(..., description="Base name without extension.")
|
||||||
filename: str = Field(..., description="Actual filename.")
|
filename: str = Field(..., description="Actual filename.")
|
||||||
content: str = Field(..., description="Raw file content.")
|
content: str = Field(..., description="Raw file content.")
|
||||||
|
|
||||||
|
class ConfFileUpdateRequest(BanGuiBaseModel):
|
||||||
class ConfFileUpdateRequest(BaseModel):
|
|
||||||
"""Payload for ``PUT /api/config/filters/{name}`` and ``PUT /api/config/actions/{name}``."""
|
"""Payload for ``PUT /api/config/filters/{name}`` and ``PUT /api/config/actions/{name}``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
content: str = Field(..., description="New raw file content (must not exceed 512 KB).")
|
content: str = Field(..., description="New raw file content (must not exceed 512 KB).")
|
||||||
|
|
||||||
|
class ConfFileCreateRequest(BanGuiBaseModel):
|
||||||
class ConfFileCreateRequest(BaseModel):
|
|
||||||
"""Payload for ``POST /api/config/filters`` and ``POST /api/config/actions``."""
|
"""Payload for ``POST /api/config/filters`` and ``POST /api/config/actions``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(
|
name: str = Field(
|
||||||
...,
|
...,
|
||||||
description="New file base name (without extension). Must contain only "
|
description="New file base name (without extension). Must contain only "
|
||||||
|
|||||||
@@ -9,21 +9,20 @@ from collections.abc import Awaitable, Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
|
||||||
|
class GeoDetail(BanGuiBaseModel):
|
||||||
class GeoDetail(BaseModel):
|
|
||||||
"""Enriched geolocation data for an IP address.
|
"""Enriched geolocation data for an IP address.
|
||||||
|
|
||||||
Populated from the ip-api.com free API.
|
Populated from the ip-api.com free API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
country_code: str | None = Field(
|
country_code: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ISO 3166-1 alpha-2 country code.",
|
description="ISO 3166-1 alpha-2 country code.",
|
||||||
@@ -41,15 +40,12 @@ class GeoDetail(BaseModel):
|
|||||||
description="Organisation associated with the ASN.",
|
description="Organisation associated with the ASN.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class GeoCacheEntry(BanGuiBaseModel):
|
||||||
class GeoCacheEntry(BaseModel):
|
|
||||||
"""A single cached geolocation entry for an IP address.
|
"""A single cached geolocation entry for an IP address.
|
||||||
|
|
||||||
Represents a row from the ``geo_cache`` table in the application database.
|
Represents a row from the ``geo_cache`` table in the application database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="IP address (IPv4 or IPv6).")
|
ip: str = Field(..., description="IP address (IPv4 or IPv6).")
|
||||||
country_code: str | None = Field(
|
country_code: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
@@ -68,43 +64,34 @@ class GeoCacheEntry(BaseModel):
|
|||||||
description="Organisation associated with the ASN.",
|
description="Organisation associated with the ASN.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class GeoCacheStatsResponse(BanGuiBaseModel):
|
||||||
class GeoCacheStatsResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/geo/stats``.
|
"""Response for ``GET /api/geo/stats``.
|
||||||
|
|
||||||
Exposes diagnostic counters of the geo cache subsystem so operators
|
Exposes diagnostic counters of the geo cache subsystem so operators
|
||||||
can assess resolution health from the UI or CLI.
|
can assess resolution health from the UI or CLI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
cache_size: int = Field(..., description="Number of positive entries in the in-memory cache.")
|
cache_size: int = Field(..., description="Number of positive entries in the in-memory cache.")
|
||||||
unresolved: int = Field(..., description="Number of geo_cache rows with country_code IS NULL.")
|
unresolved: int = Field(..., description="Number of geo_cache rows with country_code IS NULL.")
|
||||||
neg_cache_size: int = Field(..., description="Number of entries in the in-memory negative cache.")
|
neg_cache_size: int = Field(..., description="Number of entries in the in-memory negative cache.")
|
||||||
dirty_size: int = Field(..., description="Number of newly resolved entries not yet flushed to disk.")
|
dirty_size: int = Field(..., description="Number of newly resolved entries not yet flushed to disk.")
|
||||||
|
|
||||||
|
class GeoReResolveResponse(BanGuiBaseModel):
|
||||||
class GeoReResolveResponse(BaseModel):
|
|
||||||
"""Response for ``POST /api/geo/re-resolve``.
|
"""Response for ``POST /api/geo/re-resolve``.
|
||||||
|
|
||||||
Reports how many previously unresolved IPs were retried and how many
|
Reports how many previously unresolved IPs were retried and how many
|
||||||
gained a resolved country code after the re-resolve operation.
|
gained a resolved country code after the re-resolve operation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
resolved: int = Field(..., description="Number of IPs successfully resolved.")
|
resolved: int = Field(..., description="Number of IPs successfully resolved.")
|
||||||
total: int = Field(..., description="Number of IPs retried.")
|
total: int = Field(..., description="Number of IPs retried.")
|
||||||
|
|
||||||
|
class IpLookupResponse(BanGuiBaseModel):
|
||||||
class IpLookupResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/geo/lookup/{ip}``.
|
"""Response for ``GET /api/geo/lookup/{ip}``.
|
||||||
|
|
||||||
Aggregates current ban status and geographical information for an IP.
|
Aggregates current ban status and geographical information for an IP.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="The queried IP address.")
|
ip: str = Field(..., description="The queried IP address.")
|
||||||
currently_banned_in: list[str] = Field(
|
currently_banned_in: list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
@@ -115,12 +102,10 @@ class IpLookupResponse(BaseModel):
|
|||||||
description="Enriched geographical and network information.",
|
description="Enriched geographical and network information.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# shared service types
|
# shared service types
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GeoInfo:
|
class GeoInfo:
|
||||||
"""Geo resolution result used throughout backend services."""
|
"""Geo resolution result used throughout backend services."""
|
||||||
@@ -130,7 +115,6 @@ class GeoInfo:
|
|||||||
asn: str | None
|
asn: str | None
|
||||||
org: str | None
|
org: str | None
|
||||||
|
|
||||||
|
|
||||||
GeoEnricher = Callable[[str], Awaitable[GeoInfo | None]]
|
GeoEnricher = Callable[[str], Awaitable[GeoInfo | None]]
|
||||||
GeoBatchLookup = Callable[
|
GeoBatchLookup = Callable[
|
||||||
[list[str], "aiohttp.ClientSession", "aiosqlite.Connection | None"],
|
[list[str], "aiohttp.ClientSession", "aiosqlite.Connection | None"],
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ Request, response, and domain models used by the history router and service.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel
|
||||||
|
|
||||||
from app.models.ban import TimeRange
|
from app.models.ban import TimeRange
|
||||||
|
|
||||||
@@ -17,16 +19,13 @@ __all__ = [
|
|||||||
"TimeRange",
|
"TimeRange",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
class HistoryBanItem(BanGuiBaseModel):
|
||||||
class HistoryBanItem(BaseModel):
|
|
||||||
"""A single row in the history ban-list table.
|
"""A single row in the history ban-list table.
|
||||||
|
|
||||||
Populated from the fail2ban database and optionally enriched with
|
Populated from the fail2ban database and optionally enriched with
|
||||||
geolocation data.
|
geolocation data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="Banned IP address.")
|
ip: str = Field(..., description="Banned IP address.")
|
||||||
jail: str = Field(..., description="Jail that issued the ban.")
|
jail: str = Field(..., description="Jail that issued the ban.")
|
||||||
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
||||||
@@ -57,31 +56,24 @@ class HistoryBanItem(BaseModel):
|
|||||||
description="Organisation name associated with the IP.",
|
description="Organisation name associated with the IP.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class HistoryListResponse(BanGuiBaseModel):
|
||||||
class HistoryListResponse(BaseModel):
|
|
||||||
"""Paginated history ban-list response."""
|
"""Paginated history ban-list response."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
items: list[HistoryBanItem] = Field(default_factory=list)
|
items: list[HistoryBanItem] = Field(default_factory=list)
|
||||||
total: int = Field(..., ge=0, description="Total matching records.")
|
total: int = Field(..., ge=0, description="Total matching records.")
|
||||||
page: int = Field(..., ge=1)
|
page: int = Field(..., ge=1)
|
||||||
page_size: int = Field(..., ge=1)
|
page_size: int = Field(..., ge=1)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Per-IP timeline
|
# Per-IP timeline
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class IpTimelineEvent(BanGuiBaseModel):
|
||||||
class IpTimelineEvent(BaseModel):
|
|
||||||
"""A single ban event in a per-IP timeline.
|
"""A single ban event in a per-IP timeline.
|
||||||
|
|
||||||
Represents one row from the fail2ban ``bans`` table for a specific IP.
|
Represents one row from the fail2ban ``bans`` table for a specific IP.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jail: str = Field(..., description="Jail that triggered this ban.")
|
jail: str = Field(..., description="Jail that triggered this ban.")
|
||||||
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
||||||
ban_count: int = Field(
|
ban_count: int = Field(
|
||||||
@@ -99,16 +91,13 @@ class IpTimelineEvent(BaseModel):
|
|||||||
description="Matched log lines that triggered the ban.",
|
description="Matched log lines that triggered the ban.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class IpDetailResponse(BanGuiBaseModel):
|
||||||
class IpDetailResponse(BaseModel):
|
|
||||||
"""Full historical record for a single IP address.
|
"""Full historical record for a single IP address.
|
||||||
|
|
||||||
Contains aggregated totals and a chronological timeline of all ban events
|
Contains aggregated totals and a chronological timeline of all ban events
|
||||||
recorded in the fail2ban database for the given IP.
|
recorded in the fail2ban database for the given IP.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="The IP address.")
|
ip: str = Field(..., description="The IP address.")
|
||||||
total_bans: int = Field(..., ge=0, description="Total number of ban records.")
|
total_bans: int = Field(..., ge=0, description="Total number of ban records.")
|
||||||
total_failures: int = Field(
|
total_failures: int = Field(
|
||||||
|
|||||||
@@ -3,28 +3,22 @@
|
|||||||
Request, response, and domain models used by the jails router and service.
|
Request, response, and domain models used by the jails router and service.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import Field
|
||||||
|
|
||||||
from app.models.config import BantimeEscalation
|
from app.models.config import BantimeEscalation
|
||||||
from app.models.response import CommandResponse, CollectionResponse
|
from app.models.response import BanGuiBaseModel, CommandResponse, CollectionResponse
|
||||||
|
|
||||||
|
class JailStatus(BanGuiBaseModel):
|
||||||
class JailStatus(BaseModel):
|
|
||||||
"""Runtime metrics for a single jail."""
|
"""Runtime metrics for a single jail."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
currently_banned: int = Field(..., ge=0)
|
currently_banned: int = Field(..., ge=0)
|
||||||
total_banned: int = Field(..., ge=0)
|
total_banned: int = Field(..., ge=0)
|
||||||
currently_failed: int = Field(..., ge=0)
|
currently_failed: int = Field(..., ge=0)
|
||||||
total_failed: int = Field(..., ge=0)
|
total_failed: int = Field(..., ge=0)
|
||||||
|
|
||||||
|
class Jail(BanGuiBaseModel):
|
||||||
class Jail(BaseModel):
|
|
||||||
"""Domain model for a single fail2ban jail with its full configuration."""
|
"""Domain model for a single fail2ban jail with its full configuration."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str = Field(..., description="Jail name as configured in fail2ban.")
|
name: str = Field(..., description="Jail name as configured in fail2ban.")
|
||||||
enabled: bool = Field(..., description="Whether the jail is currently active.")
|
enabled: bool = Field(..., description="Whether the jail is currently active.")
|
||||||
running: bool = Field(..., description="Whether the jail backend is running.")
|
running: bool = Field(..., description="Whether the jail backend is running.")
|
||||||
@@ -46,12 +40,9 @@ class Jail(BaseModel):
|
|||||||
)
|
)
|
||||||
status: JailStatus | None = Field(default=None, description="Runtime counters.")
|
status: JailStatus | None = Field(default=None, description="Runtime counters.")
|
||||||
|
|
||||||
|
class JailSummary(BanGuiBaseModel):
|
||||||
class JailSummary(BaseModel):
|
|
||||||
"""Lightweight jail entry for the overview list."""
|
"""Lightweight jail entry for the overview list."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
enabled: bool
|
enabled: bool
|
||||||
running: bool
|
running: bool
|
||||||
@@ -62,7 +53,6 @@ class JailSummary(BaseModel):
|
|||||||
max_retry: int
|
max_retry: int
|
||||||
status: JailStatus | None = None
|
status: JailStatus | None = None
|
||||||
|
|
||||||
|
|
||||||
class JailListResponse(CollectionResponse[JailSummary]):
|
class JailListResponse(CollectionResponse[JailSummary]):
|
||||||
"""Response for ``GET /api/jails``.
|
"""Response for ``GET /api/jails``.
|
||||||
|
|
||||||
@@ -71,7 +61,6 @@ class JailListResponse(CollectionResponse[JailSummary]):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class IgnoreListResponse(CollectionResponse[str]):
|
class IgnoreListResponse(CollectionResponse[str]):
|
||||||
"""Response for ``GET /api/jails/{name}/ignoreip``.
|
"""Response for ``GET /api/jails/{name}/ignoreip``.
|
||||||
|
|
||||||
@@ -80,16 +69,13 @@ class IgnoreListResponse(CollectionResponse[str]):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class JailDetailResponse(BanGuiBaseModel):
|
||||||
class JailDetailResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/jails/{name}``.
|
"""Response for ``GET /api/jails/{name}``.
|
||||||
|
|
||||||
Includes the primary jail object together with supplemental metadata
|
Includes the primary jail object together with supplemental metadata
|
||||||
required by the UI.
|
required by the UI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
jail: Jail
|
jail: Jail
|
||||||
ignore_list: list[str] = Field(
|
ignore_list: list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
@@ -100,7 +86,6 @@ class JailDetailResponse(BaseModel):
|
|||||||
description="Whether the jail ignores the server's own IP addresses.",
|
description="Whether the jail ignores the server's own IP addresses.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class JailCommandResponse(CommandResponse):
|
class JailCommandResponse(CommandResponse):
|
||||||
"""Generic response for jail control commands (start, stop, reload, idle).
|
"""Generic response for jail control commands (start, stop, reload, idle).
|
||||||
|
|
||||||
@@ -109,10 +94,7 @@ class JailCommandResponse(CommandResponse):
|
|||||||
|
|
||||||
jail: str = Field(..., description="Target jail name, or '*' for operations on all jails.")
|
jail: str = Field(..., description="Target jail name, or '*' for operations on all jails.")
|
||||||
|
|
||||||
|
class IgnoreIpRequest(BanGuiBaseModel):
|
||||||
class IgnoreIpRequest(BaseModel):
|
|
||||||
"""Payload for adding an IP or network to a jail's ignore list."""
|
"""Payload for adding an IP or network to a jail's ignore list."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
ip: str = Field(..., description="IP address or CIDR network to ignore.")
|
ip: str = Field(..., description="IP address or CIDR network to ignore.")
|
||||||
|
|||||||
@@ -96,7 +96,27 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class PaginatedListResponse(BaseModel, Generic[T]):
|
class BanGuiBaseModel(BaseModel):
|
||||||
|
"""Project-wide Pydantic base model.
|
||||||
|
|
||||||
|
Enforces the canonical **snake_case** API field naming policy:
|
||||||
|
all JSON wire-format field names use ``snake_case`` on both the backend
|
||||||
|
(Python) and the frontend (TypeScript interfaces). No ``alias_generator``
|
||||||
|
is applied — field names are serialized exactly as written.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Every model in ``app/models/`` must inherit from this class.
|
||||||
|
- Field names must be ``snake_case`` in Python *and* in the JSON payload.
|
||||||
|
- The corresponding TypeScript interface fields must also be ``snake_case``.
|
||||||
|
- Never add a ``camelCase`` alias generator to individual models — any
|
||||||
|
serialization change must go through this base class so all models
|
||||||
|
update at once.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PaginatedListResponse(BanGuiBaseModel, Generic[T]):
|
||||||
"""Standardized paginated list response.
|
"""Standardized paginated list response.
|
||||||
|
|
||||||
Use this as a base for all endpoints that return paginated collections.
|
Use this as a base for all endpoints that return paginated collections.
|
||||||
@@ -123,15 +143,13 @@ class PaginatedListResponse(BaseModel, Generic[T]):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
items: list[T] = Field(default_factory=list, description="Data items for the current page.")
|
items: list[T] = Field(default_factory=list, description="Data items for the current page.")
|
||||||
total: int = Field(..., ge=0, description="Total number of items matching the query.")
|
total: int = Field(..., ge=0, description="Total number of items matching the query.")
|
||||||
page: int = Field(..., ge=1, description="Current page number (1-based).")
|
page: int = Field(..., ge=1, description="Current page number (1-based).")
|
||||||
page_size: int = Field(..., ge=1, description="Number of items per page.")
|
page_size: int = Field(..., ge=1, description="Number of items per page.")
|
||||||
|
|
||||||
|
|
||||||
class CollectionResponse(BaseModel, Generic[T]):
|
class CollectionResponse(BanGuiBaseModel, Generic[T]):
|
||||||
"""Standardized non-paginated collection response.
|
"""Standardized non-paginated collection response.
|
||||||
|
|
||||||
Use this for endpoints that return a collection without pagination support.
|
Use this for endpoints that return a collection without pagination support.
|
||||||
@@ -154,13 +172,11 @@ class CollectionResponse(BaseModel, Generic[T]):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
items: list[T] = Field(default_factory=list, description="Collection items.")
|
items: list[T] = Field(default_factory=list, description="Collection items.")
|
||||||
total: int = Field(..., ge=0, description="Total number of items.")
|
total: int = Field(..., ge=0, description="Total number of items.")
|
||||||
|
|
||||||
|
|
||||||
class CommandResponse(BaseModel):
|
class CommandResponse(BanGuiBaseModel):
|
||||||
"""Standardized command/action result response.
|
"""Standardized command/action result response.
|
||||||
|
|
||||||
Use this for endpoints that execute commands (start, stop, reload, ban, unban, etc.).
|
Use this for endpoints that execute commands (start, stop, reload, ban, unban, etc.).
|
||||||
@@ -184,8 +200,6 @@ class CommandResponse(BaseModel):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
message: str = Field(..., description="Human-readable result or error message.")
|
message: str = Field(..., description="Human-readable result or error message.")
|
||||||
success: bool = Field(
|
success: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
"""Server status and health-check Pydantic models.
|
"""Server status and health-check Pydantic models.
|
||||||
|
|
||||||
Used by the dashboard router, health service, and server settings router.
|
Used by the dashboard router, health service, and server settings router.
|
||||||
|
|
||||||
|
All models inherit from :class:`~app.models.response.BanGuiBaseModel` which
|
||||||
|
enforces the project-wide **snake_case** API field naming policy: field names
|
||||||
|
are identical in Python, JSON wire format, and the corresponding TypeScript
|
||||||
|
interfaces.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel
|
||||||
|
|
||||||
|
|
||||||
class ServerStatus(BaseModel):
|
class ServerStatus(BanGuiBaseModel):
|
||||||
"""Cached fail2ban server health snapshot."""
|
"""Cached fail2ban server health snapshot."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
online: bool = Field(..., description="Whether fail2ban is reachable via its socket.")
|
online: bool = Field(..., description="Whether fail2ban is reachable via its socket.")
|
||||||
version: str | None = Field(default=None, description="fail2ban version string.")
|
version: str | None = Field(default=None, description="fail2ban version string.")
|
||||||
active_jails: int = Field(default=0, ge=0, description="Number of currently active jails.")
|
active_jails: int = Field(default=0, ge=0, description="Number of currently active jails.")
|
||||||
@@ -18,19 +23,15 @@ class ServerStatus(BaseModel):
|
|||||||
total_failures: int = Field(default=0, ge=0, description="Aggregated current failure count across all jails.")
|
total_failures: int = Field(default=0, ge=0, description="Aggregated current failure count across all jails.")
|
||||||
|
|
||||||
|
|
||||||
class ServerStatusResponse(BaseModel):
|
class ServerStatusResponse(BanGuiBaseModel):
|
||||||
"""Response for ``GET /api/dashboard/status``."""
|
"""Response for ``GET /api/dashboard/status``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
status: ServerStatus
|
status: ServerStatus
|
||||||
|
|
||||||
|
|
||||||
class ServerSettings(BaseModel):
|
class ServerSettings(BanGuiBaseModel):
|
||||||
"""Domain model for fail2ban server-level settings."""
|
"""Domain model for fail2ban server-level settings."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_level: str = Field(..., description="fail2ban daemon log level.")
|
log_level: str = Field(..., description="fail2ban daemon log level.")
|
||||||
log_target: str = Field(..., description="Log destination: STDOUT, STDERR, SYSLOG, or a file path.")
|
log_target: str = Field(..., description="Log destination: STDOUT, STDERR, SYSLOG, or a file path.")
|
||||||
syslog_socket: str | None = Field(default=None)
|
syslog_socket: str | None = Field(default=None)
|
||||||
@@ -39,22 +40,18 @@ class ServerSettings(BaseModel):
|
|||||||
db_max_matches: int = Field(..., description="Maximum stored matches per ban record.")
|
db_max_matches: int = Field(..., description="Maximum stored matches per ban record.")
|
||||||
|
|
||||||
|
|
||||||
class ServerSettingsUpdate(BaseModel):
|
class ServerSettingsUpdate(BanGuiBaseModel):
|
||||||
"""Payload for ``PUT /api/server/settings``."""
|
"""Payload for ``PUT /api/server/settings``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
log_level: str | None = Field(default=None)
|
log_level: str | None = Field(default=None)
|
||||||
log_target: str | None = Field(default=None)
|
log_target: str | None = Field(default=None)
|
||||||
db_purge_age: int | None = Field(default=None, ge=0)
|
db_purge_age: int | None = Field(default=None, ge=0)
|
||||||
db_max_matches: int | None = Field(default=None, ge=0)
|
db_max_matches: int | None = Field(default=None, ge=0)
|
||||||
|
|
||||||
|
|
||||||
class ServerSettingsResponse(BaseModel):
|
class ServerSettingsResponse(BanGuiBaseModel):
|
||||||
"""Response for ``GET /api/server/settings``."""
|
"""Response for ``GET /api/server/settings``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
settings: ServerSettings
|
settings: ServerSettings
|
||||||
warnings: dict[str, bool] = Field(
|
warnings: dict[str, bool] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
Request, response, and domain models for the first-run configuration wizard.
|
Request, response, and domain models for the first-run configuration wizard.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
|
from app.models.response import BanGuiBaseModel
|
||||||
|
|
||||||
class SetupRequest(BaseModel):
|
class SetupRequest(BanGuiBaseModel):
|
||||||
"""Payload for ``POST /api/setup``."""
|
"""Payload for ``POST /api/setup``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
master_password: str = Field(
|
master_password: str = Field(
|
||||||
...,
|
...,
|
||||||
min_length=8,
|
min_length=8,
|
||||||
@@ -53,30 +52,21 @@ class SetupRequest(BaseModel):
|
|||||||
description="Number of minutes a user session remains valid.",
|
description="Number of minutes a user session remains valid.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class SetupResponse(BanGuiBaseModel):
|
||||||
class SetupResponse(BaseModel):
|
|
||||||
"""Response returned after a successful initial setup."""
|
"""Response returned after a successful initial setup."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
message: str = Field(
|
message: str = Field(
|
||||||
default="Setup completed successfully. Please log in.",
|
default="Setup completed successfully. Please log in.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class SetupTimezoneResponse(BanGuiBaseModel):
|
||||||
class SetupTimezoneResponse(BaseModel):
|
|
||||||
"""Response for ``GET /api/setup/timezone``."""
|
"""Response for ``GET /api/setup/timezone``."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
timezone: str = Field(..., description="Configured IANA timezone identifier.")
|
timezone: str = Field(..., description="Configured IANA timezone identifier.")
|
||||||
|
|
||||||
|
class SetupStatusResponse(BanGuiBaseModel):
|
||||||
class SetupStatusResponse(BaseModel):
|
|
||||||
"""Response indicating whether setup has been completed."""
|
"""Response indicating whether setup has been completed."""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
|
||||||
|
|
||||||
completed: bool = Field(
|
completed: bool = Field(
|
||||||
...,
|
...,
|
||||||
description="``True`` if the initial setup has already been performed.",
|
description="``True`` if the initial setup has already been performed.",
|
||||||
|
|||||||
Reference in New Issue
Block a user