No canonical snake_case/camelCase serialization policy
This commit is contained in:
@@ -465,18 +465,14 @@ class BansByCountryResponse(BaseModel):
|
||||
|
||||
## 5. Pydantic Models
|
||||
|
||||
- Every model inherits from `pydantic.BaseModel`.
|
||||
- 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.
|
||||
- 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.
|
||||
### Base Class
|
||||
|
||||
Every model in `app/models/` **must** inherit from `BanGuiBaseModel` (defined in `app/models/response.py`), not from `pydantic.BaseModel` directly.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from app.models.response import BanGuiBaseModel
|
||||
|
||||
class BanResponse(BaseModel):
|
||||
class BanResponse(BanGuiBaseModel):
|
||||
ip: str = Field(..., description="Banned IP address")
|
||||
jail: str = Field(..., description="Jail that issued 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")
|
||||
```
|
||||
|
||||
`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
|
||||
|
||||
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
|
||||
- Where found:
|
||||
- [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/` |
|
||||
| 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
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
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``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
password: str = Field(
|
||||
...,
|
||||
max_length=72,
|
||||
description="Master password to authenticate with (max 72 bytes due to bcrypt truncation).",
|
||||
)
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
class LoginResponse(BanGuiBaseModel):
|
||||
"""Successful login response.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
expires_at: str = Field(..., description="ISO 8601 UTC expiry timestamp.")
|
||||
|
||||
|
||||
class LogoutResponse(BaseModel):
|
||||
class LogoutResponse(BanGuiBaseModel):
|
||||
"""Response body for ``POST /api/auth/logout``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
message: str = Field(default="Logged out successfully.")
|
||||
|
||||
|
||||
class Session(BaseModel):
|
||||
class Session(BanGuiBaseModel):
|
||||
"""Internal domain model representing a persisted session record."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
id: int = Field(..., description="Auto-incremented row ID.")
|
||||
token: str = Field(..., description="Opaque session token.")
|
||||
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
|
||||
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
|
||||
@@ -25,21 +26,15 @@ TIME_RANGE_SECONDS: dict[str, int] = {
|
||||
"365d": 365 * 24 * 3600,
|
||||
}
|
||||
|
||||
|
||||
class BanRequest(BaseModel):
|
||||
class BanRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/bans`` (ban an IP)."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="IP address to ban.")
|
||||
jail: str = Field(..., description="Jail in which to apply the ban.")
|
||||
|
||||
|
||||
class UnbanRequest(BaseModel):
|
||||
class UnbanRequest(BanGuiBaseModel):
|
||||
"""Payload for ``DELETE /api/bans`` (unban an IP)."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="IP address to unban.")
|
||||
jail: str | None = Field(
|
||||
default=None,
|
||||
@@ -50,14 +45,12 @@ class UnbanRequest(BaseModel):
|
||||
description="When ``true`` the IP is unbanned from every jail.",
|
||||
)
|
||||
|
||||
|
||||
#: Discriminator literal for the origin of a ban.
|
||||
BanOrigin = Literal["blocklist", "selfblock"]
|
||||
|
||||
#: Jail name used by the blocklist import service.
|
||||
BLOCKLIST_JAIL: str = "blocklist-import"
|
||||
|
||||
|
||||
def _derive_origin(jail: str) -> BanOrigin:
|
||||
"""Derive the ban origin from the jail name.
|
||||
|
||||
@@ -70,12 +63,9 @@ def _derive_origin(jail: str) -> BanOrigin:
|
||||
"""
|
||||
return "blocklist" if jail == BLOCKLIST_JAIL else "selfblock"
|
||||
|
||||
|
||||
class Ban(BaseModel):
|
||||
class Ban(BanGuiBaseModel):
|
||||
"""Domain model representing a single active or historical ban record."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="Banned IP address.")
|
||||
jail: str = Field(..., description="Jail that issued 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.",
|
||||
)
|
||||
|
||||
|
||||
class BanResponse(BaseModel):
|
||||
class BanResponse(BanGuiBaseModel):
|
||||
"""Response containing a single ban record."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ban: Ban
|
||||
|
||||
|
||||
class BanListResponse(PaginatedListResponse[Ban]):
|
||||
"""Paginated list of ban records.
|
||||
|
||||
@@ -114,12 +100,9 @@ class BanListResponse(PaginatedListResponse[Ban]):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ActiveBan(BaseModel):
|
||||
class ActiveBan(BanGuiBaseModel):
|
||||
"""A currently active ban entry returned by ``GET /api/bans/active``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="Banned IP address.")
|
||||
jail: str = Field(..., description="Jail holding 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.")
|
||||
country: str | None = Field(default=None, description="ISO 3166-1 alpha-2 country code.")
|
||||
|
||||
|
||||
class ActiveBanListResponse(CollectionResponse[ActiveBan]):
|
||||
"""List of all currently active bans across all jails.
|
||||
|
||||
@@ -143,29 +125,22 @@ class ActiveBanListResponse(CollectionResponse[ActiveBan]):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnbanAllResponse(BaseModel):
|
||||
class UnbanAllResponse(BanGuiBaseModel):
|
||||
"""Response for ``DELETE /api/bans/all``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
message: str = Field(..., description="Human-readable summary of the operation.")
|
||||
count: int = Field(..., ge=0, description="Number of IPs that were unbanned.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dashboard ban-list view models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DashboardBanItem(BaseModel):
|
||||
class DashboardBanItem(BanGuiBaseModel):
|
||||
"""A single row in the dashboard ban-list table.
|
||||
|
||||
Populated from the fail2ban database and enriched with geo data.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="Banned IP address.")
|
||||
jail: str = Field(..., description="Jail that issued 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.",
|
||||
)
|
||||
|
||||
|
||||
class DashboardBanListResponse(PaginatedListResponse[DashboardBanItem]):
|
||||
"""Paginated dashboard ban-list response.
|
||||
|
||||
@@ -205,8 +179,7 @@ class DashboardBanListResponse(PaginatedListResponse[DashboardBanItem]):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BansByCountryResponse(BaseModel):
|
||||
class BansByCountryResponse(BanGuiBaseModel):
|
||||
"""Response for the bans-by-country aggregation endpoint.
|
||||
|
||||
Contains a per-country ban count, a human-readable country name map, and
|
||||
@@ -215,8 +188,6 @@ class BansByCountryResponse(BaseModel):
|
||||
single request.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
countries: dict[str, int] = Field(
|
||||
default_factory=dict,
|
||||
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.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trend endpoint models
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -252,7 +222,6 @@ BUCKET_SIZE_LABEL: dict[str, str] = {
|
||||
"365d": "7d",
|
||||
}
|
||||
|
||||
|
||||
def bucket_count(range_: TimeRange) -> int:
|
||||
"""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_])
|
||||
|
||||
|
||||
class BanTrendBucket(BaseModel):
|
||||
class BanTrendBucket(BanGuiBaseModel):
|
||||
"""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.")
|
||||
count: int = Field(..., ge=0, description="Number of bans that started in this bucket.")
|
||||
|
||||
|
||||
class BanTrendResponse(BaseModel):
|
||||
class BanTrendResponse(BanGuiBaseModel):
|
||||
"""Response for the ``GET /api/dashboard/bans/trend`` endpoint."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
buckets: list[BanTrendBucket] = Field(
|
||||
default_factory=list,
|
||||
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').",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# By-jail endpoint models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailBanCount(BaseModel):
|
||||
class JailBanCount(BanGuiBaseModel):
|
||||
"""A single jail entry in the bans-by-jail aggregation."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
jail: str = Field(..., description="Jail name.")
|
||||
count: int = Field(..., ge=0, description="Number of bans recorded in this jail.")
|
||||
|
||||
|
||||
class BansByJailResponse(BaseModel):
|
||||
class BansByJailResponse(BanGuiBaseModel):
|
||||
"""Response for the ``GET /api/dashboard/bans/by-jail`` endpoint."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
jails: list[JailBanCount] = Field(
|
||||
default_factory=list,
|
||||
description="Jails ordered by ban count descending.",
|
||||
)
|
||||
total: int = Field(..., ge=0, description="Total ban count in the selected window.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jail-specific paginated bans
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailBannedIpsResponse(PaginatedListResponse[ActiveBan]):
|
||||
"""Paginated response for ``GET /api/jails/{name}/banned``.
|
||||
|
||||
|
||||
@@ -8,18 +8,18 @@ from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field
|
||||
from pydantic import AnyHttpUrl, Field
|
||||
|
||||
from app.models.response import BanGuiBaseModel
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blocklist source
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BlocklistSource(BaseModel):
|
||||
class BlocklistSource(BanGuiBaseModel):
|
||||
"""Domain model for a blocklist source definition."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
url: str
|
||||
@@ -27,8 +27,7 @@ class BlocklistSource(BaseModel):
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class BlocklistSourceCreate(BaseModel):
|
||||
class BlocklistSourceCreate(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/blocklists``.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
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).")
|
||||
enabled: bool = Field(default=True)
|
||||
|
||||
|
||||
class BlocklistSourceUpdate(BaseModel):
|
||||
class BlocklistSourceUpdate(BanGuiBaseModel):
|
||||
"""Payload for ``PUT /api/blocklists/{id}``. All fields are optional.
|
||||
|
||||
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)
|
||||
url: AnyHttpUrl | None = Field(default=None)
|
||||
enabled: bool | None = Field(default=None)
|
||||
|
||||
|
||||
class BlocklistListResponse(BaseModel):
|
||||
class BlocklistListResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/blocklists``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
sources: list[BlocklistSource] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Import log
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ImportLogEntry(BaseModel):
|
||||
class ImportLogEntry(BanGuiBaseModel):
|
||||
"""A single blocklist import run record."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
id: int
|
||||
source_id: int | None
|
||||
source_url: str
|
||||
@@ -82,24 +69,19 @@ class ImportLogEntry(BaseModel):
|
||||
ips_skipped: int
|
||||
errors: str | None
|
||||
|
||||
|
||||
class ImportLogListResponse(BaseModel):
|
||||
class ImportLogListResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/blocklists/log``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
items: list[ImportLogEntry] = Field(default_factory=list)
|
||||
total: int = Field(..., ge=0)
|
||||
page: int = Field(default=1, ge=1)
|
||||
page_size: int = Field(default=50, ge=1)
|
||||
total_pages: int = Field(default=1, ge=1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schedule
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ScheduleFrequency(StrEnum):
|
||||
"""Available import schedule frequency presets."""
|
||||
|
||||
@@ -107,8 +89,7 @@ class ScheduleFrequency(StrEnum):
|
||||
daily = "daily"
|
||||
weekly = "weekly"
|
||||
|
||||
|
||||
class ScheduleConfig(BaseModel):
|
||||
class ScheduleConfig(BanGuiBaseModel):
|
||||
"""Import schedule configuration.
|
||||
|
||||
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)",
|
||||
)
|
||||
|
||||
|
||||
class ScheduleInfo(BaseModel):
|
||||
class ScheduleInfo(BanGuiBaseModel):
|
||||
"""Current schedule configuration together with runtime metadata."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
config: ScheduleConfig
|
||||
next_run_at: str | None
|
||||
last_run_at: str | None
|
||||
last_run_errors: bool | None = None
|
||||
"""``True`` if the most recent import had errors, ``False`` if clean, ``None`` if never run."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Import results
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ImportSourceResult(BaseModel):
|
||||
class ImportSourceResult(BanGuiBaseModel):
|
||||
"""Result of importing a single blocklist source."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
source_id: int | None
|
||||
source_url: str
|
||||
ips_imported: int
|
||||
ips_skipped: int
|
||||
error: str | None
|
||||
|
||||
|
||||
class ImportRunResult(BaseModel):
|
||||
class ImportRunResult(BanGuiBaseModel):
|
||||
"""Aggregated result from a full import run across all enabled sources."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
results: list[ImportSourceResult] = Field(default_factory=list)
|
||||
total_imported: int
|
||||
total_skipped: int
|
||||
errors_count: int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Preview
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class PreviewResponse(BaseModel):
|
||||
class PreviewResponse(BanGuiBaseModel):
|
||||
"""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")
|
||||
total_lines: int
|
||||
valid_count: int
|
||||
|
||||
@@ -7,10 +7,10 @@ import datetime
|
||||
from pathlib import Path
|
||||
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.models.response import CollectionResponse
|
||||
from app.models.response import BanGuiBaseModel, CollectionResponse
|
||||
from app.utils.path_utils import validate_log_path
|
||||
|
||||
DNSMode = Literal["yes", "warn", "no", "raw"]
|
||||
@@ -23,12 +23,9 @@ LogTarget = Literal["STDOUT", "STDERR", "SYSLOG"]
|
||||
# Ban-time escalation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BantimeEscalation(BaseModel):
|
||||
class BantimeEscalation(BanGuiBaseModel):
|
||||
"""Incremental ban-time escalation configuration for a jail."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
increment: bool = Field(
|
||||
default=False,
|
||||
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.",
|
||||
)
|
||||
|
||||
|
||||
class BantimeEscalationUpdate(BaseModel):
|
||||
class BantimeEscalationUpdate(BanGuiBaseModel):
|
||||
"""Partial update payload for ban-time escalation settings."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
increment: bool | None = Field(default=None)
|
||||
factor: float | None = Field(default=None)
|
||||
formula: str | None = Field(default=None)
|
||||
@@ -72,17 +66,13 @@ class BantimeEscalationUpdate(BaseModel):
|
||||
rnd_time: int | None = Field(default=None)
|
||||
overall_jails: bool | None = Field(default=None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jail configuration models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailConfig(BaseModel):
|
||||
class JailConfig(BanGuiBaseModel):
|
||||
"""Configuration snapshot of a single jail (editable fields)."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Jail name as configured in fail2ban.")
|
||||
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.")
|
||||
@@ -101,15 +91,11 @@ class JailConfig(BaseModel):
|
||||
description="Incremental ban-time escalation settings, or None if not configured.",
|
||||
)
|
||||
|
||||
|
||||
class JailConfigResponse(BaseModel):
|
||||
class JailConfigResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/config/jails/{name}``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
jail: JailConfig
|
||||
|
||||
|
||||
class JailConfigListResponse(CollectionResponse[JailConfig]):
|
||||
"""Response for ``GET /api/config/jails``.
|
||||
|
||||
@@ -118,12 +104,9 @@ class JailConfigListResponse(CollectionResponse[JailConfig]):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class JailConfigUpdate(BaseModel):
|
||||
class JailConfigUpdate(BanGuiBaseModel):
|
||||
"""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.")
|
||||
max_retry: 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.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regex tester models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RegexTestRequest(BaseModel):
|
||||
class RegexTestRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/config/regex-test``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
log_line: str = Field(..., description="Sample log line to test against.")
|
||||
fail_regex: str = Field(..., description="Regex pattern to match.")
|
||||
|
||||
|
||||
class RegexTestResponse(BaseModel):
|
||||
class RegexTestResponse(BanGuiBaseModel):
|
||||
"""Result of a regex test."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
matched: bool = Field(..., description="Whether the pattern matched the log line.")
|
||||
groups: list[str] = Field(
|
||||
default_factory=list,
|
||||
@@ -170,17 +146,13 @@ class RegexTestResponse(BaseModel):
|
||||
description="Compilation error message if the regex is invalid.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Global config models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GlobalConfigResponse(BaseModel):
|
||||
class GlobalConfigResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/config/global``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
log_level: LogLevel
|
||||
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.")
|
||||
@@ -222,12 +194,9 @@ class GlobalConfigResponse(BaseModel):
|
||||
f"Log target {value!r} is outside allowed directories: {allowed_dirs_str}"
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfigUpdate(BaseModel):
|
||||
class GlobalConfigUpdate(BanGuiBaseModel):
|
||||
"""Payload for ``PUT /api/config/global``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
log_level: LogLevel | None = Field(
|
||||
default=None,
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Log observation / preview models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AddLogPathRequest(BaseModel):
|
||||
class AddLogPathRequest(BanGuiBaseModel):
|
||||
"""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.")
|
||||
tail: bool = Field(
|
||||
default=True,
|
||||
@@ -311,49 +276,35 @@ class AddLogPathRequest(BaseModel):
|
||||
"""
|
||||
return validate_log_path(value)
|
||||
|
||||
|
||||
|
||||
class LogPreviewRequest(BaseModel):
|
||||
class LogPreviewRequest(BanGuiBaseModel):
|
||||
"""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.")
|
||||
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.")
|
||||
|
||||
|
||||
class LogPreviewLine(BaseModel):
|
||||
class LogPreviewLine(BanGuiBaseModel):
|
||||
"""A single log line with match information."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
line: str
|
||||
matched: bool
|
||||
groups: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class LogPreviewResponse(BaseModel):
|
||||
class LogPreviewResponse(BanGuiBaseModel):
|
||||
"""Response for ``POST /api/config/preview-log``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
lines: list[LogPreviewLine] = Field(default_factory=list)
|
||||
total_lines: int = Field(..., ge=0)
|
||||
matched_count: int = Field(..., ge=0)
|
||||
regex_error: str | None = Field(default=None, description="Set if the regex failed to compile.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Map color threshold models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MapColorThresholdsResponse(BaseModel):
|
||||
class MapColorThresholdsResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/config/map-thresholds``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
threshold_high: int = Field(
|
||||
..., description="Ban count for red coloring."
|
||||
)
|
||||
@@ -364,25 +315,20 @@ class MapColorThresholdsResponse(BaseModel):
|
||||
..., description="Ban count for green coloring."
|
||||
)
|
||||
|
||||
|
||||
class MapColorThresholdsUpdate(BaseModel):
|
||||
class MapColorThresholdsUpdate(BanGuiBaseModel):
|
||||
"""Payload for ``PUT /api/config/map-thresholds``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
threshold_high: int = Field(..., gt=0, description="Ban count for red.")
|
||||
threshold_medium: int = Field(
|
||||
..., gt=0, description="Ban count for yellow."
|
||||
)
|
||||
threshold_low: int = Field(..., gt=0, description="Ban count for green.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsed filter file models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FilterConfig(BaseModel):
|
||||
class FilterConfig(BanGuiBaseModel):
|
||||
"""Structured representation of a ``filter.d/*.conf`` file.
|
||||
|
||||
The ``active``, ``used_by_jails``, ``source_file``, and
|
||||
@@ -393,8 +339,6 @@ class FilterConfig(BaseModel):
|
||||
these fields carry their default values.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Filter base name, e.g. ``sshd``.")
|
||||
filename: str = Field(..., description="Actual filename, e.g. ``sshd.conf``.")
|
||||
# [INCLUDES]
|
||||
@@ -458,15 +402,12 @@ class FilterConfig(BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class FilterConfigUpdate(BaseModel):
|
||||
class FilterConfigUpdate(BanGuiBaseModel):
|
||||
"""Partial update payload for a parsed filter file.
|
||||
|
||||
Only explicitly set (non-``None``) fields are written back.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
before: str | None = Field(default=None)
|
||||
after: 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)
|
||||
journalmatch: str | None = Field(default=None)
|
||||
|
||||
|
||||
class FilterUpdateRequest(BaseModel):
|
||||
class FilterUpdateRequest(BanGuiBaseModel):
|
||||
"""Payload for ``PUT /api/config/filters/{name}``.
|
||||
|
||||
Accepts only the user-editable ``[Definition]`` fields. Fields left as
|
||||
@@ -486,8 +426,6 @@ class FilterUpdateRequest(BaseModel):
|
||||
preserved.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
failregex: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Updated failure-detection regex patterns. ``None`` = keep existing.",
|
||||
@@ -505,15 +443,12 @@ class FilterUpdateRequest(BaseModel):
|
||||
description="Systemd journal match expression. ``None`` = keep existing.",
|
||||
)
|
||||
|
||||
|
||||
class FilterCreateRequest(BaseModel):
|
||||
class FilterCreateRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/config/filters``.
|
||||
|
||||
Creates a new user-defined filter at ``filter.d/{name}.local``.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(
|
||||
...,
|
||||
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.",
|
||||
)
|
||||
|
||||
|
||||
class AssignFilterRequest(BaseModel):
|
||||
class AssignFilterRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/config/jails/{jail_name}/filter``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
filter_name: str = Field(
|
||||
...,
|
||||
description="Filter base name to assign to the jail (e.g. ``sshd``).",
|
||||
)
|
||||
|
||||
|
||||
class FilterListResponse(BaseModel):
|
||||
class FilterListResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/config/filters``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
filters: list[FilterConfig] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
@@ -565,17 +494,13 @@ class FilterListResponse(BaseModel):
|
||||
)
|
||||
total: int = Field(..., ge=0, description="Total number of filters found.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsed action file models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ActionConfig(BaseModel):
|
||||
class ActionConfig(BanGuiBaseModel):
|
||||
"""Structured representation of an ``action.d/*.conf`` file."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Action base name, e.g. ``iptables``.")
|
||||
filename: str = Field(..., description="Actual filename, e.g. ``iptables.conf``.")
|
||||
# [INCLUDES]
|
||||
@@ -644,12 +569,9 @@ class ActionConfig(BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ActionConfigUpdate(BaseModel):
|
||||
class ActionConfigUpdate(BanGuiBaseModel):
|
||||
"""Partial update payload for a parsed action file."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
before: str | None = Field(default=None)
|
||||
after: 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)
|
||||
init_vars: dict[str, str] | None = Field(default=None)
|
||||
|
||||
|
||||
class ActionListResponse(BaseModel):
|
||||
class ActionListResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/config/actions``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
actions: list[ActionConfig] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
@@ -676,16 +595,13 @@ class ActionListResponse(BaseModel):
|
||||
)
|
||||
total: int = Field(..., ge=0, description="Total number of actions found.")
|
||||
|
||||
|
||||
class ActionUpdateRequest(BaseModel):
|
||||
class ActionUpdateRequest(BanGuiBaseModel):
|
||||
"""Payload for ``PUT /api/config/actions/{name}``.
|
||||
|
||||
Accepts only the user-editable ``[Definition]`` lifecycle fields and
|
||||
``[Init]`` parameters. Fields left as ``None`` are not changed.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
actionstart: str | None = Field(
|
||||
default=None,
|
||||
description="Updated ``actionstart`` command. ``None`` = keep existing.",
|
||||
@@ -719,15 +635,12 @@ class ActionUpdateRequest(BaseModel):
|
||||
description="``[Init]`` parameters to set. ``None`` = keep existing.",
|
||||
)
|
||||
|
||||
|
||||
class ActionCreateRequest(BaseModel):
|
||||
class ActionCreateRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/config/actions``.
|
||||
|
||||
Creates a new user-defined action at ``action.d/{name}.local``.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(
|
||||
...,
|
||||
description="Action base name (e.g. ``my-custom-action``). Must not already exist.",
|
||||
@@ -747,12 +660,9 @@ class ActionCreateRequest(BaseModel):
|
||||
description="``[Init]`` runtime parameters.",
|
||||
)
|
||||
|
||||
|
||||
class AssignActionRequest(BaseModel):
|
||||
class AssignActionRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/config/jails/{jail_name}/action``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
action_name: str = Field(
|
||||
...,
|
||||
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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailSectionConfig(BaseModel):
|
||||
class JailSectionConfig(BanGuiBaseModel):
|
||||
"""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.")
|
||||
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').")
|
||||
@@ -787,36 +693,28 @@ class JailSectionConfig(BaseModel):
|
||||
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.")
|
||||
|
||||
|
||||
class JailFileConfig(BaseModel):
|
||||
class JailFileConfig(BanGuiBaseModel):
|
||||
"""Structured representation of a jail.d/*.conf file."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
filename: str = Field(..., description="Filename including extension (e.g. 'sshd.conf').")
|
||||
jails: dict[str, JailSectionConfig] = Field(
|
||||
default_factory=dict,
|
||||
description="Mapping of jail name → settings for each [section] in the file.",
|
||||
)
|
||||
|
||||
|
||||
class JailFileConfigUpdate(BaseModel):
|
||||
class JailFileConfigUpdate(BanGuiBaseModel):
|
||||
"""Partial update payload for a jail.d file."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
jails: dict[str, JailSectionConfig] | None = Field(
|
||||
default=None,
|
||||
description="Jail section updates. Only jails present in this dict are updated.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Inactive jail models (Stage 1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class InactiveJail(BaseModel):
|
||||
class InactiveJail(BanGuiBaseModel):
|
||||
"""A jail defined in fail2ban config files that is not currently active.
|
||||
|
||||
A jail is considered inactive when its ``enabled`` key is ``false`` (or
|
||||
@@ -825,8 +723,6 @@ class InactiveJail(BaseModel):
|
||||
running.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Jail name from the config section header.")
|
||||
filter: str = Field(
|
||||
...,
|
||||
@@ -920,7 +816,6 @@ class InactiveJail(BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class InactiveJailListResponse(CollectionResponse[InactiveJail]):
|
||||
"""Response for ``GET /api/config/jails/inactive``.
|
||||
|
||||
@@ -929,8 +824,7 @@ class InactiveJailListResponse(CollectionResponse[InactiveJail]):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ActivateJailRequest(BaseModel):
|
||||
class ActivateJailRequest(BanGuiBaseModel):
|
||||
"""Optional override values when activating an inactive jail.
|
||||
|
||||
All fields are optional. Omitted fields are not written to the
|
||||
@@ -938,8 +832,6 @@ class ActivateJailRequest(BaseModel):
|
||||
values.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
bantime: str | None = Field(
|
||||
default=None,
|
||||
description="Override ban duration, e.g. ``1h`` or ``3600``.",
|
||||
@@ -962,12 +854,9 @@ class ActivateJailRequest(BaseModel):
|
||||
description="Override log file paths.",
|
||||
)
|
||||
|
||||
|
||||
class JailActivationResponse(BaseModel):
|
||||
class JailActivationResponse(BanGuiBaseModel):
|
||||
"""Response for jail activation and deactivation endpoints."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Name of the affected jail.")
|
||||
active: bool = Field(
|
||||
...,
|
||||
@@ -996,29 +885,22 @@ class JailActivationResponse(BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jail validation models (Task 3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailValidationIssue(BaseModel):
|
||||
class JailValidationIssue(BanGuiBaseModel):
|
||||
"""A single issue found during pre-activation validation of a jail config."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
field: str = Field(
|
||||
...,
|
||||
description="Config field associated with this issue, e.g. 'filter', 'failregex', 'logpath'.",
|
||||
)
|
||||
message: str = Field(..., description="Human-readable description of the issue.")
|
||||
|
||||
|
||||
class JailValidationResult(BaseModel):
|
||||
class JailValidationResult(BanGuiBaseModel):
|
||||
"""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.")
|
||||
valid: bool = Field(..., description="True when no issues were found.")
|
||||
issues: list[JailValidationIssue] = Field(
|
||||
@@ -1026,17 +908,13 @@ class JailValidationResult(BaseModel):
|
||||
description="Validation issues found. Empty when valid=True.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rollback response model (Task 3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RollbackResponse(BaseModel):
|
||||
class RollbackResponse(BanGuiBaseModel):
|
||||
"""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.")
|
||||
disabled: bool = Field(
|
||||
...,
|
||||
@@ -1053,17 +931,13 @@ class RollbackResponse(BaseModel):
|
||||
)
|
||||
message: str = Field(..., description="Human-readable result message.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pending recovery model (Task 3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class PendingRecovery(BaseModel):
|
||||
class PendingRecovery(BanGuiBaseModel):
|
||||
"""Records a probable activation-caused fail2ban crash pending user action."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
jail_name: str = Field(
|
||||
...,
|
||||
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.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# fail2ban log viewer models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Fail2BanLogResponse(BaseModel):
|
||||
class Fail2BanLogResponse(BanGuiBaseModel):
|
||||
"""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.")
|
||||
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.")
|
||||
log_level: str = Field(..., description="Current fail2ban log level.")
|
||||
log_target: str = Field(..., description="Current fail2ban log target (file path or special value).")
|
||||
|
||||
|
||||
class ServiceStatusResponse(BaseModel):
|
||||
class ServiceStatusResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/config/service-status``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
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).")
|
||||
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/``).
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import Field
|
||||
|
||||
from app.models.response import BanGuiBaseModel
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jail config file models (Task 4a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailConfigFile(BaseModel):
|
||||
class JailConfigFile(BanGuiBaseModel):
|
||||
"""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``).")
|
||||
filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).")
|
||||
enabled: bool = Field(
|
||||
@@ -26,81 +26,56 @@ class JailConfigFile(BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class JailConfigFilesResponse(BaseModel):
|
||||
class JailConfigFilesResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/config/jail-files``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
files: list[JailConfigFile] = Field(default_factory=list)
|
||||
total: int = Field(..., ge=0)
|
||||
|
||||
|
||||
class JailConfigFileContent(BaseModel):
|
||||
class JailConfigFileContent(BanGuiBaseModel):
|
||||
"""Single jail config file with its raw content."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Jail name (file stem).")
|
||||
filename: str = Field(..., description="Actual filename.")
|
||||
enabled: bool = Field(..., description="Whether the jail is enabled.")
|
||||
content: str = Field(..., description="Raw file content.")
|
||||
|
||||
|
||||
class JailConfigFileEnabledUpdate(BaseModel):
|
||||
class JailConfigFileEnabledUpdate(BanGuiBaseModel):
|
||||
"""Payload for ``PUT /api/config/jail-files/{filename}/enabled``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
enabled: bool = Field(..., description="New enabled state for this jail.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generic conf-file entry (shared by filter.d and action.d)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ConfFileEntry(BaseModel):
|
||||
class ConfFileEntry(BanGuiBaseModel):
|
||||
"""Metadata for a single ``.conf`` or ``.local`` file."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Base name without extension (e.g. ``sshd``).")
|
||||
filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).")
|
||||
|
||||
|
||||
class ConfFilesResponse(BaseModel):
|
||||
class ConfFilesResponse(BanGuiBaseModel):
|
||||
"""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)
|
||||
total: int = Field(..., ge=0)
|
||||
|
||||
|
||||
class ConfFileContent(BaseModel):
|
||||
class ConfFileContent(BanGuiBaseModel):
|
||||
"""A conf file with its raw text content."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(..., description="Base name without extension.")
|
||||
filename: str = Field(..., description="Actual filename.")
|
||||
content: str = Field(..., description="Raw file content.")
|
||||
|
||||
|
||||
class ConfFileUpdateRequest(BaseModel):
|
||||
class ConfFileUpdateRequest(BanGuiBaseModel):
|
||||
"""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).")
|
||||
|
||||
|
||||
class ConfFileCreateRequest(BaseModel):
|
||||
class ConfFileCreateRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/config/filters`` and ``POST /api/config/actions``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str = Field(
|
||||
...,
|
||||
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 typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import Field
|
||||
|
||||
from app.models.response import BanGuiBaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import aiohttp
|
||||
import aiosqlite
|
||||
|
||||
|
||||
class GeoDetail(BaseModel):
|
||||
class GeoDetail(BanGuiBaseModel):
|
||||
"""Enriched geolocation data for an IP address.
|
||||
|
||||
Populated from the ip-api.com free API.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
country_code: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 3166-1 alpha-2 country code.",
|
||||
@@ -41,15 +40,12 @@ class GeoDetail(BaseModel):
|
||||
description="Organisation associated with the ASN.",
|
||||
)
|
||||
|
||||
|
||||
class GeoCacheEntry(BaseModel):
|
||||
class GeoCacheEntry(BanGuiBaseModel):
|
||||
"""A single cached geolocation entry for an IP address.
|
||||
|
||||
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).")
|
||||
country_code: str | None = Field(
|
||||
default=None,
|
||||
@@ -68,43 +64,34 @@ class GeoCacheEntry(BaseModel):
|
||||
description="Organisation associated with the ASN.",
|
||||
)
|
||||
|
||||
|
||||
class GeoCacheStatsResponse(BaseModel):
|
||||
class GeoCacheStatsResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/geo/stats``.
|
||||
|
||||
Exposes diagnostic counters of the geo cache subsystem so operators
|
||||
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.")
|
||||
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.")
|
||||
dirty_size: int = Field(..., description="Number of newly resolved entries not yet flushed to disk.")
|
||||
|
||||
|
||||
class GeoReResolveResponse(BaseModel):
|
||||
class GeoReResolveResponse(BanGuiBaseModel):
|
||||
"""Response for ``POST /api/geo/re-resolve``.
|
||||
|
||||
Reports how many previously unresolved IPs were retried and how many
|
||||
gained a resolved country code after the re-resolve operation.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
resolved: int = Field(..., description="Number of IPs successfully resolved.")
|
||||
total: int = Field(..., description="Number of IPs retried.")
|
||||
|
||||
|
||||
class IpLookupResponse(BaseModel):
|
||||
class IpLookupResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/geo/lookup/{ip}``.
|
||||
|
||||
Aggregates current ban status and geographical information for an IP.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="The queried IP address.")
|
||||
currently_banned_in: list[str] = Field(
|
||||
default_factory=list,
|
||||
@@ -115,12 +102,10 @@ class IpLookupResponse(BaseModel):
|
||||
description="Enriched geographical and network information.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# shared service types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeoInfo:
|
||||
"""Geo resolution result used throughout backend services."""
|
||||
@@ -130,7 +115,6 @@ class GeoInfo:
|
||||
asn: str | None
|
||||
org: str | None
|
||||
|
||||
|
||||
GeoEnricher = Callable[[str], Awaitable[GeoInfo | None]]
|
||||
GeoBatchLookup = Callable[
|
||||
[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 pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import Field
|
||||
|
||||
from app.models.response import BanGuiBaseModel
|
||||
|
||||
from app.models.ban import TimeRange
|
||||
|
||||
@@ -17,16 +19,13 @@ __all__ = [
|
||||
"TimeRange",
|
||||
]
|
||||
|
||||
|
||||
class HistoryBanItem(BaseModel):
|
||||
class HistoryBanItem(BanGuiBaseModel):
|
||||
"""A single row in the history ban-list table.
|
||||
|
||||
Populated from the fail2ban database and optionally enriched with
|
||||
geolocation data.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="Banned IP address.")
|
||||
jail: str = Field(..., description="Jail that issued 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.",
|
||||
)
|
||||
|
||||
|
||||
class HistoryListResponse(BaseModel):
|
||||
class HistoryListResponse(BanGuiBaseModel):
|
||||
"""Paginated history ban-list response."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
items: list[HistoryBanItem] = Field(default_factory=list)
|
||||
total: int = Field(..., ge=0, description="Total matching records.")
|
||||
page: int = Field(..., ge=1)
|
||||
page_size: int = Field(..., ge=1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-IP timeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class IpTimelineEvent(BaseModel):
|
||||
class IpTimelineEvent(BanGuiBaseModel):
|
||||
"""A single ban event in a per-IP timeline.
|
||||
|
||||
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.")
|
||||
banned_at: str = Field(..., description="ISO 8601 UTC timestamp of the ban.")
|
||||
ban_count: int = Field(
|
||||
@@ -99,16 +91,13 @@ class IpTimelineEvent(BaseModel):
|
||||
description="Matched log lines that triggered the ban.",
|
||||
)
|
||||
|
||||
|
||||
class IpDetailResponse(BaseModel):
|
||||
class IpDetailResponse(BanGuiBaseModel):
|
||||
"""Full historical record for a single IP address.
|
||||
|
||||
Contains aggregated totals and a chronological timeline of all ban events
|
||||
recorded in the fail2ban database for the given IP.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str = Field(..., description="The IP address.")
|
||||
total_bans: int = Field(..., ge=0, description="Total number of ban records.")
|
||||
total_failures: int = Field(
|
||||
|
||||
@@ -3,28 +3,22 @@
|
||||
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.response import CommandResponse, CollectionResponse
|
||||
from app.models.response import BanGuiBaseModel, CommandResponse, CollectionResponse
|
||||
|
||||
|
||||
class JailStatus(BaseModel):
|
||||
class JailStatus(BanGuiBaseModel):
|
||||
"""Runtime metrics for a single jail."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
currently_banned: int = Field(..., ge=0)
|
||||
total_banned: int = Field(..., ge=0)
|
||||
currently_failed: int = Field(..., ge=0)
|
||||
total_failed: int = Field(..., ge=0)
|
||||
|
||||
|
||||
class Jail(BaseModel):
|
||||
class Jail(BanGuiBaseModel):
|
||||
"""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.")
|
||||
enabled: bool = Field(..., description="Whether the jail is currently active.")
|
||||
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.")
|
||||
|
||||
|
||||
class JailSummary(BaseModel):
|
||||
class JailSummary(BanGuiBaseModel):
|
||||
"""Lightweight jail entry for the overview list."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
name: str
|
||||
enabled: bool
|
||||
running: bool
|
||||
@@ -62,7 +53,6 @@ class JailSummary(BaseModel):
|
||||
max_retry: int
|
||||
status: JailStatus | None = None
|
||||
|
||||
|
||||
class JailListResponse(CollectionResponse[JailSummary]):
|
||||
"""Response for ``GET /api/jails``.
|
||||
|
||||
@@ -71,7 +61,6 @@ class JailListResponse(CollectionResponse[JailSummary]):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IgnoreListResponse(CollectionResponse[str]):
|
||||
"""Response for ``GET /api/jails/{name}/ignoreip``.
|
||||
|
||||
@@ -80,16 +69,13 @@ class IgnoreListResponse(CollectionResponse[str]):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class JailDetailResponse(BaseModel):
|
||||
class JailDetailResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/jails/{name}``.
|
||||
|
||||
Includes the primary jail object together with supplemental metadata
|
||||
required by the UI.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
jail: Jail
|
||||
ignore_list: list[str] = Field(
|
||||
default_factory=list,
|
||||
@@ -100,7 +86,6 @@ class JailDetailResponse(BaseModel):
|
||||
description="Whether the jail ignores the server's own IP addresses.",
|
||||
)
|
||||
|
||||
|
||||
class JailCommandResponse(CommandResponse):
|
||||
"""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.")
|
||||
|
||||
|
||||
class IgnoreIpRequest(BaseModel):
|
||||
class IgnoreIpRequest(BanGuiBaseModel):
|
||||
"""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.")
|
||||
|
||||
@@ -96,7 +96,27 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
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.
|
||||
|
||||
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.")
|
||||
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_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.
|
||||
|
||||
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.")
|
||||
total: int = Field(..., ge=0, description="Total number of items.")
|
||||
|
||||
|
||||
class CommandResponse(BaseModel):
|
||||
class CommandResponse(BanGuiBaseModel):
|
||||
"""Standardized command/action result response.
|
||||
|
||||
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.")
|
||||
success: bool = Field(
|
||||
default=True,
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
"""Server status and health-check Pydantic models.
|
||||
|
||||
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."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
online: bool = Field(..., description="Whether fail2ban is reachable via its socket.")
|
||||
version: str | None = Field(default=None, description="fail2ban version string.")
|
||||
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.")
|
||||
|
||||
|
||||
class ServerStatusResponse(BaseModel):
|
||||
class ServerStatusResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/dashboard/status``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
status: ServerStatus
|
||||
|
||||
|
||||
class ServerSettings(BaseModel):
|
||||
class ServerSettings(BanGuiBaseModel):
|
||||
"""Domain model for fail2ban server-level settings."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
log_level: str = Field(..., description="fail2ban daemon log level.")
|
||||
log_target: str = Field(..., description="Log destination: STDOUT, STDERR, SYSLOG, or a file path.")
|
||||
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.")
|
||||
|
||||
|
||||
class ServerSettingsUpdate(BaseModel):
|
||||
class ServerSettingsUpdate(BanGuiBaseModel):
|
||||
"""Payload for ``PUT /api/server/settings``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
log_level: str | None = Field(default=None)
|
||||
log_target: str | None = Field(default=None)
|
||||
db_purge_age: 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``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
settings: ServerSettings
|
||||
warnings: dict[str, bool] = Field(
|
||||
default_factory=dict,
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
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``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
master_password: str = Field(
|
||||
...,
|
||||
min_length=8,
|
||||
@@ -53,30 +52,21 @@ class SetupRequest(BaseModel):
|
||||
description="Number of minutes a user session remains valid.",
|
||||
)
|
||||
|
||||
|
||||
class SetupResponse(BaseModel):
|
||||
class SetupResponse(BanGuiBaseModel):
|
||||
"""Response returned after a successful initial setup."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
message: str = Field(
|
||||
default="Setup completed successfully. Please log in.",
|
||||
)
|
||||
|
||||
|
||||
class SetupTimezoneResponse(BaseModel):
|
||||
class SetupTimezoneResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/setup/timezone``."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
timezone: str = Field(..., description="Configured IANA timezone identifier.")
|
||||
|
||||
|
||||
class SetupStatusResponse(BaseModel):
|
||||
class SetupStatusResponse(BanGuiBaseModel):
|
||||
"""Response indicating whether setup has been completed."""
|
||||
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
completed: bool = Field(
|
||||
...,
|
||||
description="``True`` if the initial setup has already been performed.",
|
||||
|
||||
Reference in New Issue
Block a user