Standardize API response envelopes: use items for collection responses and update tests
This commit is contained in:
@@ -224,6 +224,7 @@ async def get_jails(state: JailServiceStateDep) -> JailListResponse:
|
|||||||
- Use appropriate HTTP status codes: `201` for creation, `204` for deletion with no body, `404` for not found, etc.
|
- Use appropriate HTTP status codes: `201` for creation, `204` for deletion with no body, `404` for not found, etc.
|
||||||
- Protected endpoints should return `401 Unauthorized` or `403 Forbidden` when the session is invalid or expired; the frontend treats these responses as a session-expiry event and redirects the user to `/login`.
|
- Protected endpoints should return `401 Unauthorized` or `403 Forbidden` when the session is invalid or expired; the frontend treats these responses as a session-expiry event and redirects the user to `/login`.
|
||||||
- Use **HTTPException** or custom exception handlers — never return error dicts manually.
|
- Use **HTTPException** or custom exception handlers — never return error dicts manually.
|
||||||
|
- All successful responses must use a standardized Pydantic response model. List and collection endpoints should wrap data in `items`, `total`, and optional pagination metadata. Detail endpoints must expose a single domain object under a named field (for example `jail`, `status`, or `settings`). Command endpoints must use a `CommandResponse`-style wrapper with `message` and `success`.
|
||||||
- **GET endpoints are read-only — never call `db.commit()` or execute INSERT/UPDATE/DELETE inside a GET handler.** If a GET path produces side-effects (e.g., caching resolved data), that write belongs in a background task, a scheduled flush, or a separate POST endpoint. Users and HTTP caches assume GET is idempotent and non-mutating.
|
- **GET endpoints are read-only — never call `db.commit()` or execute INSERT/UPDATE/DELETE inside a GET handler.** If a GET path produces side-effects (e.g., caching resolved data), that write belongs in a background task, a scheduled flush, or a separate POST endpoint. Users and HTTP caches assume GET is idempotent and non-mutating.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -72,12 +72,33 @@ class JailListResponse(CollectionResponse[JailSummary]):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IgnoreListResponse(CollectionResponse[str]):
|
||||||
|
"""Response for ``GET /api/jails/{name}/ignoreip``.
|
||||||
|
|
||||||
|
Returns the jailed ignore list as a standard collection response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class JailDetailResponse(BaseModel):
|
class JailDetailResponse(BaseModel):
|
||||||
"""Response for ``GET /api/jails/{name}``."""
|
"""Response for ``GET /api/jails/{name}``.
|
||||||
|
|
||||||
|
Includes the primary jail object together with supplemental metadata
|
||||||
|
required by the UI.
|
||||||
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(strict=True)
|
model_config = ConfigDict(strict=True)
|
||||||
|
|
||||||
jail: Jail
|
jail: Jail
|
||||||
|
ignore_list: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="List of IP addresses and networks currently ignored by the jail.",
|
||||||
|
)
|
||||||
|
ignore_self: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Whether the jail ignores the server's own IP addresses.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JailCommandResponse(CommandResponse):
|
class JailCommandResponse(CommandResponse):
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Provides CRUD and control operations for fail2ban jails:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, HTTPException, Path, status
|
from fastapi import APIRouter, Body, HTTPException, Path, status
|
||||||
@@ -34,6 +35,7 @@ from app.dependencies import (
|
|||||||
from app.models.ban import JailBannedIpsResponse
|
from app.models.ban import JailBannedIpsResponse
|
||||||
from app.models.jail import (
|
from app.models.jail import (
|
||||||
IgnoreIpRequest,
|
IgnoreIpRequest,
|
||||||
|
IgnoreListResponse,
|
||||||
JailCommandResponse,
|
JailCommandResponse,
|
||||||
JailDetailResponse,
|
JailDetailResponse,
|
||||||
JailListResponse,
|
JailListResponse,
|
||||||
@@ -103,7 +105,16 @@ async def get_jail(
|
|||||||
HTTPException: 404 when the jail does not exist.
|
HTTPException: 404 when the jail does not exist.
|
||||||
HTTPException: 502 when fail2ban is unreachable.
|
HTTPException: 502 when fail2ban is unreachable.
|
||||||
"""
|
"""
|
||||||
return await jail_service.get_jail(socket_path, name)
|
jail, ignore_list, ignore_self = await asyncio.gather(
|
||||||
|
jail_service.get_jail(socket_path, name),
|
||||||
|
jail_service.get_ignore_list(socket_path, name),
|
||||||
|
jail_service.get_ignore_self(socket_path, name),
|
||||||
|
)
|
||||||
|
return JailDetailResponse(
|
||||||
|
jail=jail,
|
||||||
|
ignore_list=ignore_list,
|
||||||
|
ignore_self=ignore_self,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -278,14 +289,14 @@ class _IgnoreSelfRequest(IgnoreIpRequest):
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{name}/ignoreip",
|
"/{name}/ignoreip",
|
||||||
response_model=list[str],
|
response_model=IgnoreListResponse,
|
||||||
summary="List the ignore IPs for a jail",
|
summary="List the ignore IPs for a jail",
|
||||||
)
|
)
|
||||||
async def get_ignore_list(
|
async def get_ignore_list(
|
||||||
_auth: AuthDep,
|
_auth: AuthDep,
|
||||||
name: _NamePath,
|
name: _NamePath,
|
||||||
socket_path: Fail2BanSocketDep,
|
socket_path: Fail2BanSocketDep,
|
||||||
) -> list[str]:
|
) -> IgnoreListResponse:
|
||||||
"""Return the current ignore list (IP whitelist) for a fail2ban jail.
|
"""Return the current ignore list (IP whitelist) for a fail2ban jail.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -299,7 +310,8 @@ async def get_ignore_list(
|
|||||||
HTTPException: 404 when the jail does not exist.
|
HTTPException: 404 when the jail does not exist.
|
||||||
HTTPException: 502 when fail2ban is unreachable.
|
HTTPException: 502 when fail2ban is unreachable.
|
||||||
"""
|
"""
|
||||||
return await jail_service.get_ignore_list(socket_path, name)
|
ignore_list = await jail_service.get_ignore_list(socket_path, name)
|
||||||
|
return IgnoreListResponse(items=ignore_list, total=len(ignore_list))
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ async def list_jail_configs(socket_path: str) -> JailConfigListResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not jail_names:
|
if not jail_names:
|
||||||
return JailConfigListResponse(jails=[], total=0)
|
return JailConfigListResponse(items=[], total=0)
|
||||||
|
|
||||||
responses: list[JailConfigResponse] = await asyncio.gather(
|
responses: list[JailConfigResponse] = await asyncio.gather(
|
||||||
*[get_jail_config(socket_path, name) for name in jail_names],
|
*[get_jail_config(socket_path, name) for name in jail_names],
|
||||||
@@ -227,7 +227,7 @@ async def list_jail_configs(socket_path: str) -> JailConfigListResponse:
|
|||||||
|
|
||||||
jails = [r.jail for r in responses]
|
jails = [r.jail for r in responses]
|
||||||
log.info("jail_configs_listed", count=len(jails))
|
log.info("jail_configs_listed", count=len(jails))
|
||||||
return JailConfigListResponse(jails=jails, total=len(jails))
|
return JailConfigListResponse(items=jails, total=len(jails))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ async def list_inactive_jails(
|
|||||||
active=len(active_names),
|
active=len(active_names),
|
||||||
inactive=len(inactive),
|
inactive=len(inactive),
|
||||||
)
|
)
|
||||||
return InactiveJailListResponse(jails=inactive, total=len(inactive))
|
return InactiveJailListResponse(items=inactive, total=len(inactive))
|
||||||
|
|
||||||
|
|
||||||
async def activate_jail(
|
async def activate_jail(
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ async def config_client(tmp_path: Path) -> AsyncClient: # type: ignore[misc]
|
|||||||
session_duration_minutes=60,
|
session_duration_minutes=60,
|
||||||
timezone="UTC",
|
timezone="UTC",
|
||||||
log_level="debug",
|
log_level="debug",
|
||||||
|
session_cookie_secure=False,
|
||||||
)
|
)
|
||||||
app = create_app(settings=settings)
|
app = create_app(settings=settings)
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ class TestGetJailConfigs:
|
|||||||
async def test_200_returns_jail_list(self, config_client: AsyncClient) -> None:
|
async def test_200_returns_jail_list(self, config_client: AsyncClient) -> None:
|
||||||
"""GET /api/config/jails returns 200 with JailConfigListResponse."""
|
"""GET /api/config/jails returns 200 with JailConfigListResponse."""
|
||||||
mock_response = JailConfigListResponse(
|
mock_response = JailConfigListResponse(
|
||||||
jails=[_make_jail_config("sshd")], total=1
|
items=[_make_jail_config("sshd")], total=1
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
"app.routers.jail_config.config_service.list_jail_configs",
|
"app.routers.jail_config.config_service.list_jail_configs",
|
||||||
@@ -109,7 +110,7 @@ class TestGetJailConfigs:
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
assert data["total"] == 1
|
assert data["total"] == 1
|
||||||
assert data["jails"][0]["name"] == "sshd"
|
assert data["items"][0]["name"] == "sshd"
|
||||||
|
|
||||||
async def test_401_when_unauthenticated(self, config_client: AsyncClient) -> None:
|
async def test_401_when_unauthenticated(self, config_client: AsyncClient) -> None:
|
||||||
"""GET /api/config/jails returns 401 without a valid session."""
|
"""GET /api/config/jails returns 401 without a valid session."""
|
||||||
@@ -694,7 +695,7 @@ class TestGetInactiveJails:
|
|||||||
source_file="/etc/fail2ban/jail.conf",
|
source_file="/etc/fail2ban/jail.conf",
|
||||||
enabled=False,
|
enabled=False,
|
||||||
)
|
)
|
||||||
mock_response = InactiveJailListResponse(jails=[mock_jail], total=1)
|
mock_response = InactiveJailListResponse(items=[mock_jail], total=1)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"app.routers.jail_config.jail_config_service.list_inactive_jails",
|
"app.routers.jail_config.jail_config_service.list_inactive_jails",
|
||||||
@@ -705,7 +706,7 @@ class TestGetInactiveJails:
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
assert data["total"] == 1
|
assert data["total"] == 1
|
||||||
assert data["jails"][0]["name"] == "apache-auth"
|
assert data["items"][0]["name"] == "apache-auth"
|
||||||
|
|
||||||
async def test_200_empty_list(self, config_client: AsyncClient) -> None:
|
async def test_200_empty_list(self, config_client: AsyncClient) -> None:
|
||||||
"""GET /api/config/jails/inactive returns 200 with empty list."""
|
"""GET /api/config/jails/inactive returns 200 with empty list."""
|
||||||
@@ -713,13 +714,13 @@ class TestGetInactiveJails:
|
|||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"app.routers.jail_config.jail_config_service.list_inactive_jails",
|
"app.routers.jail_config.jail_config_service.list_inactive_jails",
|
||||||
AsyncMock(return_value=InactiveJailListResponse(jails=[], total=0)),
|
AsyncMock(return_value=InactiveJailListResponse(items=[], total=0)),
|
||||||
):
|
):
|
||||||
resp = await config_client.get("/api/config/jails/inactive")
|
resp = await config_client.get("/api/config/jails/inactive")
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["total"] == 0
|
assert resp.json()["total"] == 0
|
||||||
assert resp.json()["jails"] == []
|
assert resp.json()["items"] == []
|
||||||
|
|
||||||
async def test_401_when_unauthenticated(self, config_client: AsyncClient) -> None:
|
async def test_401_when_unauthenticated(self, config_client: AsyncClient) -> None:
|
||||||
"""GET /api/config/jails/inactive returns 401 without a valid session."""
|
"""GET /api/config/jails/inactive returns 401 without a valid session."""
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class FakeAuthService:
|
|||||||
|
|
||||||
class FakeJailService:
|
class FakeJailService:
|
||||||
async def list_jails(self, _socket_path: str) -> JailListResponse:
|
async def list_jails(self, _socket_path: str) -> JailListResponse:
|
||||||
return JailListResponse(jails=[], total=0)
|
return JailListResponse(items=[], total=0)
|
||||||
|
|
||||||
async def get_jail(self, _socket_path: str, _name: str) -> JailListResponse:
|
async def get_jail(self, _socket_path: str, _name: str) -> JailListResponse:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ async def jails_client(tmp_path: Path) -> AsyncClient: # type: ignore[misc]
|
|||||||
settings = Settings(
|
settings = Settings(
|
||||||
database_path=str(tmp_path / "jails_test.db"),
|
database_path=str(tmp_path / "jails_test.db"),
|
||||||
fail2ban_socket="/tmp/fake.sock",
|
fail2ban_socket="/tmp/fake.sock",
|
||||||
session_secret="test-jails-secret",
|
session_secret="test-jails-secret-0000000000000000000000",
|
||||||
session_duration_minutes=60,
|
session_duration_minutes=60,
|
||||||
timezone="UTC",
|
timezone="UTC",
|
||||||
log_level="debug",
|
log_level="debug",
|
||||||
@@ -108,7 +108,9 @@ def _detail(name: str = "sshd") -> JailDetailResponse:
|
|||||||
currently_failed=1,
|
currently_failed=1,
|
||||||
total_failed=50,
|
total_failed=50,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
ignore_list=["127.0.0.1"],
|
||||||
|
ignore_self=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ class TestGetJails:
|
|||||||
|
|
||||||
async def test_200_when_authenticated(self, jails_client: AsyncClient) -> None:
|
async def test_200_when_authenticated(self, jails_client: AsyncClient) -> None:
|
||||||
"""GET /api/jails returns 200 with a JailListResponse."""
|
"""GET /api/jails returns 200 with a JailListResponse."""
|
||||||
mock_response = JailListResponse(jails=[_summary()], total=1)
|
mock_response = JailListResponse(items=[_summary()], total=1)
|
||||||
with patch(
|
with patch(
|
||||||
"app.routers.jails.jail_service.list_jails",
|
"app.routers.jails.jail_service.list_jails",
|
||||||
AsyncMock(return_value=mock_response),
|
AsyncMock(return_value=mock_response),
|
||||||
@@ -132,7 +134,7 @@ class TestGetJails:
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
assert data["total"] == 1
|
assert data["total"] == 1
|
||||||
assert data["jails"][0]["name"] == "sshd"
|
assert data["items"][0]["name"] == "sshd"
|
||||||
|
|
||||||
async def test_401_when_unauthenticated(self, jails_client: AsyncClient) -> None:
|
async def test_401_when_unauthenticated(self, jails_client: AsyncClient) -> None:
|
||||||
"""GET /api/jails returns 401 without a session cookie."""
|
"""GET /api/jails returns 401 without a session cookie."""
|
||||||
@@ -144,14 +146,14 @@ class TestGetJails:
|
|||||||
|
|
||||||
async def test_response_shape(self, jails_client: AsyncClient) -> None:
|
async def test_response_shape(self, jails_client: AsyncClient) -> None:
|
||||||
"""GET /api/jails response contains expected fields."""
|
"""GET /api/jails response contains expected fields."""
|
||||||
mock_response = JailListResponse(jails=[_summary()], total=1)
|
mock_response = JailListResponse(items=[_summary()], total=1)
|
||||||
with patch(
|
with patch(
|
||||||
"app.routers.jails.jail_service.list_jails",
|
"app.routers.jails.jail_service.list_jails",
|
||||||
AsyncMock(return_value=mock_response),
|
AsyncMock(return_value=mock_response),
|
||||||
):
|
):
|
||||||
resp = await jails_client.get("/api/jails")
|
resp = await jails_client.get("/api/jails")
|
||||||
|
|
||||||
jail = resp.json()["jails"][0]
|
jail = resp.json()["items"][0]
|
||||||
assert "name" in jail
|
assert "name" in jail
|
||||||
assert "enabled" in jail
|
assert "enabled" in jail
|
||||||
assert "running" in jail
|
assert "running" in jail
|
||||||
@@ -359,7 +361,7 @@ class TestIgnoreIpEndpoints:
|
|||||||
"""Tests for ignore-list management endpoints."""
|
"""Tests for ignore-list management endpoints."""
|
||||||
|
|
||||||
async def test_get_ignore_list(self, jails_client: AsyncClient) -> None:
|
async def test_get_ignore_list(self, jails_client: AsyncClient) -> None:
|
||||||
"""GET /api/jails/sshd/ignoreip returns 200 with a list."""
|
"""GET /api/jails/sshd/ignoreip returns 200 with a wrapped response."""
|
||||||
with patch(
|
with patch(
|
||||||
"app.routers.jails.jail_service.get_ignore_list",
|
"app.routers.jails.jail_service.get_ignore_list",
|
||||||
AsyncMock(return_value=["127.0.0.1"]),
|
AsyncMock(return_value=["127.0.0.1"]),
|
||||||
@@ -367,7 +369,7 @@ class TestIgnoreIpEndpoints:
|
|||||||
resp = await jails_client.get("/api/jails/sshd/ignoreip")
|
resp = await jails_client.get("/api/jails/sshd/ignoreip")
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert "127.0.0.1" in resp.json()
|
assert resp.json() == {"items": ["127.0.0.1"], "total": 1}
|
||||||
|
|
||||||
async def test_add_ignore_ip_returns_201(self, jails_client: AsyncClient) -> None:
|
async def test_add_ignore_ip_returns_201(self, jails_client: AsyncClient) -> None:
|
||||||
"""POST /api/jails/sshd/ignoreip returns 201 on success."""
|
"""POST /api/jails/sshd/ignoreip returns 201 on success."""
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ class TestListInactiveJails:
|
|||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
|
|
||||||
names = [j.name for j in result.jails]
|
names = [j.name for j in result.items]
|
||||||
assert "sshd" not in names
|
assert "sshd" not in names
|
||||||
assert "apache-auth" in names
|
assert "apache-auth" in names
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@ class TestListInactiveJails:
|
|||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
|
|
||||||
assert result.total == len(result.jails)
|
assert result.total == len(result.items)
|
||||||
|
|
||||||
async def test_empty_config_dir(self, tmp_path: Path) -> None:
|
async def test_empty_config_dir(self, tmp_path: Path) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
@@ -402,7 +402,7 @@ class TestListInactiveJails:
|
|||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
|
|
||||||
assert result.jails == []
|
assert result.items == []
|
||||||
assert result.total == 0
|
assert result.total == 0
|
||||||
|
|
||||||
async def test_all_active_returns_empty(self, tmp_path: Path) -> None:
|
async def test_all_active_returns_empty(self, tmp_path: Path) -> None:
|
||||||
@@ -413,7 +413,7 @@ class TestListInactiveJails:
|
|||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
|
|
||||||
assert result.jails == []
|
assert result.items == []
|
||||||
|
|
||||||
async def test_fail2ban_unreachable_shows_all(self, tmp_path: Path) -> None:
|
async def test_fail2ban_unreachable_shows_all(self, tmp_path: Path) -> None:
|
||||||
# When fail2ban is unreachable, _get_active_jail_names returns empty set,
|
# When fail2ban is unreachable, _get_active_jail_names returns empty set,
|
||||||
@@ -425,7 +425,7 @@ class TestListInactiveJails:
|
|||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
|
|
||||||
names = {j.name for j in result.jails}
|
names = {j.name for j in result.items}
|
||||||
assert "sshd" in names
|
assert "sshd" in names
|
||||||
assert "apache-auth" in names
|
assert "apache-auth" in names
|
||||||
|
|
||||||
@@ -440,7 +440,7 @@ class TestListInactiveJails:
|
|||||||
new=AsyncMock(return_value=set()),
|
new=AsyncMock(return_value=set()),
|
||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
jail = next(j for j in result.jails if j.name == "apache-auth")
|
jail = next(j for j in result.items if j.name == "apache-auth")
|
||||||
assert jail.has_local_override is True
|
assert jail.has_local_override is True
|
||||||
|
|
||||||
async def test_has_local_override_false_when_no_local_file(self, tmp_path: Path) -> None:
|
async def test_has_local_override_false_when_no_local_file(self, tmp_path: Path) -> None:
|
||||||
@@ -451,7 +451,7 @@ class TestListInactiveJails:
|
|||||||
new=AsyncMock(return_value=set()),
|
new=AsyncMock(return_value=set()),
|
||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
jail = next(j for j in result.jails if j.name == "apache-auth")
|
jail = next(j for j in result.items if j.name == "apache-auth")
|
||||||
assert jail.has_local_override is False
|
assert jail.has_local_override is False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ class TestListJailConfigs:
|
|||||||
|
|
||||||
assert isinstance(result, JailConfigListResponse)
|
assert isinstance(result, JailConfigListResponse)
|
||||||
assert result.total == 1
|
assert result.total == 1
|
||||||
assert result.jails[0].name == "sshd"
|
assert result.items[0].name == "sshd"
|
||||||
|
|
||||||
async def test_empty_when_no_jails(self) -> None:
|
async def test_empty_when_no_jails(self) -> None:
|
||||||
"""list_jail_configs returns empty list when no jails are active."""
|
"""list_jail_configs returns empty list when no jails are active."""
|
||||||
@@ -229,7 +229,7 @@ class TestListJailConfigs:
|
|||||||
result = await config_service.list_jail_configs(_SOCKET)
|
result = await config_service.list_jail_configs(_SOCKET)
|
||||||
|
|
||||||
assert result.total == 0
|
assert result.total == 0
|
||||||
assert result.jails == []
|
assert result.items == []
|
||||||
|
|
||||||
async def test_multiple_jails(self) -> None:
|
async def test_multiple_jails(self) -> None:
|
||||||
"""list_jail_configs handles comma-separated jail names."""
|
"""list_jail_configs handles comma-separated jail names."""
|
||||||
@@ -245,7 +245,7 @@ class TestListJailConfigs:
|
|||||||
result = await config_service.list_jail_configs(_SOCKET)
|
result = await config_service.list_jail_configs(_SOCKET)
|
||||||
|
|
||||||
assert result.total == 2
|
assert result.total == 2
|
||||||
names = {j.name for j in result.jails}
|
names = {j.name for j in result.items}
|
||||||
assert names == {"sshd", "nginx"}
|
assert names == {"sshd", "nginx"}
|
||||||
|
|
||||||
|
|
||||||
@@ -887,7 +887,7 @@ class TestConfigModuleIntegration:
|
|||||||
):
|
):
|
||||||
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
result = await list_inactive_jails(str(tmp_path), "/fake.sock")
|
||||||
|
|
||||||
names = {j.name for j in result.jails}
|
names = {j.name for j in result.items}
|
||||||
assert "apache-auth" in names
|
assert "apache-auth" in names
|
||||||
assert "sshd" not in names
|
assert "sshd" not in names
|
||||||
|
|
||||||
|
|||||||
25
frontend/src/api/__tests__/jails.test.ts
Normal file
25
frontend/src/api/__tests__/jails.test.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { Mock } from "vitest";
|
||||||
|
import { ENDPOINTS } from "../endpoints";
|
||||||
|
import { fetchIgnoreList } from "../jails";
|
||||||
|
import { get } from "../client";
|
||||||
|
|
||||||
|
vi.mock("../client", () => ({
|
||||||
|
get: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedGet = get as Mock;
|
||||||
|
|
||||||
|
describe("fetchIgnoreList", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedGet.mockReset();
|
||||||
|
mockedGet.mockResolvedValue({ items: ["127.0.0.1"], total: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests the jail ignore list endpoint and returns the wrapped response", async () => {
|
||||||
|
const response = await fetchIgnoreList("sshd");
|
||||||
|
|
||||||
|
expect(get).toHaveBeenCalledWith(ENDPOINTS.jailIgnoreIp("sshd"));
|
||||||
|
expect(response).toEqual({ items: ["127.0.0.1"], total: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import { del, get, post } from "./client";
|
|||||||
import { ENDPOINTS } from "./endpoints";
|
import { ENDPOINTS } from "./endpoints";
|
||||||
import type {
|
import type {
|
||||||
ActiveBanListResponse,
|
ActiveBanListResponse,
|
||||||
|
IgnoreListResponse,
|
||||||
IpLookupResponse,
|
IpLookupResponse,
|
||||||
JailBannedIpsResponse,
|
JailBannedIpsResponse,
|
||||||
JailCommandResponse,
|
JailCommandResponse,
|
||||||
@@ -112,11 +113,11 @@ export async function reloadAllJails(): Promise<JailCommandResponse> {
|
|||||||
* Return the ignore list for a jail.
|
* Return the ignore list for a jail.
|
||||||
*
|
*
|
||||||
* @param name - Jail name.
|
* @param name - Jail name.
|
||||||
* @returns Array of IP addresses / CIDR networks on the ignore list.
|
* @returns The {@link IgnoreListResponse} wrapper containing the ignore list.
|
||||||
* @throws {ApiError} On non-2xx responses.
|
* @throws {ApiError} On non-2xx responses.
|
||||||
*/
|
*/
|
||||||
export async function fetchIgnoreList(name: string): Promise<string[]> {
|
export async function fetchIgnoreList(name: string): Promise<IgnoreListResponse> {
|
||||||
return get<string[]>(ENDPOINTS.jailIgnoreIp(name));
|
return get<IgnoreListResponse>(ENDPOINTS.jailIgnoreIp(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ vi.mock("../../api/config", () => ({
|
|||||||
createActionFile: vi.fn(),
|
createActionFile: vi.fn(),
|
||||||
previewLog: vi.fn(),
|
previewLog: vi.fn(),
|
||||||
testRegex: vi.fn(),
|
testRegex: vi.fn(),
|
||||||
fetchInactiveJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
|
fetchInactiveJails: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
||||||
activateJail: vi.fn(),
|
activateJail: vi.fn(),
|
||||||
deactivateJail: vi.fn(),
|
deactivateJail: vi.fn(),
|
||||||
fetchParsedFilter: vi.fn(),
|
fetchParsedFilter: vi.fn(),
|
||||||
@@ -139,7 +139,7 @@ vi.mock("../../api/config", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../api/jails", () => ({
|
vi.mock("../../api/jails", () => ({
|
||||||
fetchJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
|
fetchJails: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/** Minimal jail fixture used across tests. */
|
/** Minimal jail fixture used across tests. */
|
||||||
@@ -185,7 +185,7 @@ async function openSshdAccordion(user: ReturnType<typeof userEvent.setup>) {
|
|||||||
describe("ConfigPage — Add Log Path", () => {
|
describe("ConfigPage — Add Log Path", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockFetchJailConfigs.mockResolvedValue({ jails: [MOCK_JAIL], total: 1 });
|
mockFetchJailConfigs.mockResolvedValue({ items: [MOCK_JAIL], total: 1 });
|
||||||
mockAddLogPath.mockResolvedValue(undefined);
|
mockAddLogPath.mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const mockFetchJails = vi.mocked(fetchJails);
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const mockJailsResponse: JailListResponse = {
|
const mockJailsResponse: JailListResponse = {
|
||||||
jails: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "sshd",
|
name: "sshd",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ vi.mock("../../../hooks/useAutoSave");
|
|||||||
vi.mock("../../../hooks/useJailConfigs");
|
vi.mock("../../../hooks/useJailConfigs");
|
||||||
vi.mock("../../../hooks/useConfigActiveStatus");
|
vi.mock("../../../hooks/useConfigActiveStatus");
|
||||||
vi.mock("../../../api/config", () => ({
|
vi.mock("../../../api/config", () => ({
|
||||||
fetchInactiveJails: vi.fn().mockResolvedValue({ jails: [] }),
|
fetchInactiveJails: vi.fn().mockResolvedValue({ items: [] }),
|
||||||
deactivateJail: vi.fn(),
|
deactivateJail: vi.fn(),
|
||||||
deleteJailLocalOverride: vi.fn(),
|
deleteJailLocalOverride: vi.fn(),
|
||||||
addLogPath: vi.fn(),
|
addLogPath: vi.fn(),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe("useJailBannedIps", () => {
|
|||||||
const unbanMock = vi.mocked(api.unbanIp);
|
const unbanMock = vi.mocked(api.unbanIp);
|
||||||
|
|
||||||
fetchMock.mockResolvedValue({ items: [{ ip: "1.2.3.4", jail: "sshd", banned_at: "2025-01-01T10:00:00+00:00", expires_at: "2025-01-01T10:10:00+00:00", ban_count: 1, country: "US" }], total: 1, page: 1, page_size: 25 });
|
fetchMock.mockResolvedValue({ items: [{ ip: "1.2.3.4", jail: "sshd", banned_at: "2025-01-01T10:00:00+00:00", expires_at: "2025-01-01T10:10:00+00:00", ban_count: 1, country: "US" }], total: 1, page: 1, page_size: 25 });
|
||||||
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd" });
|
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
|
|
||||||
const { result } = renderHook(() => useJailBannedIps("sshd"));
|
const { result } = renderHook(() => useJailBannedIps("sshd"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("useJailConfigs", () => {
|
|||||||
|
|
||||||
it("calls fetchJailConfigs only once on mount", async () => {
|
it("calls fetchJailConfigs only once on mount", async () => {
|
||||||
vi.mocked(configApi.fetchJailConfigs).mockResolvedValue({
|
vi.mocked(configApi.fetchJailConfigs).mockResolvedValue({
|
||||||
jails: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "sshd",
|
name: "sshd",
|
||||||
ban_time: 600,
|
ban_time: 600,
|
||||||
@@ -49,7 +49,7 @@ describe("useJailConfigs", () => {
|
|||||||
|
|
||||||
it("does not trigger infinite refetch with stable onSuccess", async () => {
|
it("does not trigger infinite refetch with stable onSuccess", async () => {
|
||||||
vi.mocked(configApi.fetchJailConfigs).mockResolvedValue({
|
vi.mocked(configApi.fetchJailConfigs).mockResolvedValue({
|
||||||
jails: [],
|
items: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ vi.mock("../../api/jails");
|
|||||||
|
|
||||||
const mockJail: Jail = {
|
const mockJail: Jail = {
|
||||||
name: "sshd",
|
name: "sshd",
|
||||||
|
enabled: true,
|
||||||
running: true,
|
running: true,
|
||||||
idle: false,
|
idle: false,
|
||||||
backend: "pyinotify",
|
backend: "pyinotify",
|
||||||
log_paths: ["/var/log/auth.log"],
|
log_paths: ["/var/log/auth.log"],
|
||||||
fail_regex: ["^\\[.*\\]\\s.*Failed password"],
|
fail_regex: ["^\[.*\]\s.*Failed password"],
|
||||||
ignore_regex: [],
|
ignore_regex: [],
|
||||||
|
ignore_ips: [],
|
||||||
date_pattern: "%b %d %H:%M:%S",
|
date_pattern: "%b %d %H:%M:%S",
|
||||||
log_encoding: "UTF-8",
|
log_encoding: "UTF-8",
|
||||||
actions: [],
|
actions: [],
|
||||||
@@ -87,13 +89,13 @@ describe("useJailCommands — write operations", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
vi.mocked(jailsApi.startJail).mockResolvedValue({ message: "ok", jail: "sshd" });
|
vi.mocked(jailsApi.startJail).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
vi.mocked(jailsApi.stopJail).mockResolvedValue({ message: "ok", jail: "sshd" });
|
vi.mocked(jailsApi.stopJail).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
vi.mocked(jailsApi.reloadJail).mockResolvedValue({ message: "ok", jail: "sshd" });
|
vi.mocked(jailsApi.reloadJail).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
vi.mocked(jailsApi.setJailIdle).mockResolvedValue({ message: "ok", jail: "sshd" });
|
vi.mocked(jailsApi.setJailIdle).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
vi.mocked(jailsApi.addIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd" });
|
vi.mocked(jailsApi.addIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
vi.mocked(jailsApi.delIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd" });
|
vi.mocked(jailsApi.delIgnoreIp).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
vi.mocked(jailsApi.toggleIgnoreSelf).mockResolvedValue({ message: "ok", jail: "sshd" });
|
vi.mocked(jailsApi.toggleIgnoreSelf).mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls start() API and invokes onSuccess", async () => {
|
it("calls start() API and invokes onSuccess", async () => {
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ vi.mock("../../hooks/useJailBannedIps", () => ({
|
|||||||
|
|
||||||
// Mock API functions used by JailInfoSection control buttons to avoid side effects.
|
// Mock API functions used by JailInfoSection control buttons to avoid side effects.
|
||||||
vi.mock("../../api/jails", () => ({
|
vi.mock("../../api/jails", () => ({
|
||||||
startJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
startJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||||
stopJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
stopJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||||
reloadJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
reloadJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||||
setJailIdle: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
setJailIdle: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||||
toggleIgnoreSelf: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }),
|
toggleIgnoreSelf: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd", success: true }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Stub BannedIpsSection to prevent its own fetchJailBannedIps calls.
|
// Stub BannedIpsSection to prevent its own fetchJailBannedIps calls.
|
||||||
@@ -92,12 +92,14 @@ import { useJailCommands } from "../../hooks/useJailCommands";
|
|||||||
function makeJail(): Jail {
|
function makeJail(): Jail {
|
||||||
return {
|
return {
|
||||||
name: "sshd",
|
name: "sshd",
|
||||||
|
enabled: true,
|
||||||
running: true,
|
running: true,
|
||||||
idle: false,
|
idle: false,
|
||||||
backend: "systemd",
|
backend: "systemd",
|
||||||
log_paths: ["/var/log/auth.log"],
|
log_paths: ["/var/log/auth.log"],
|
||||||
fail_regex: ["^Failed .+ from <HOST>"],
|
fail_regex: ["^Failed .+ from <HOST>"],
|
||||||
ignore_regex: [],
|
ignore_regex: [],
|
||||||
|
ignore_ips: [],
|
||||||
date_pattern: "",
|
date_pattern: "",
|
||||||
log_encoding: "UTF-8",
|
log_encoding: "UTF-8",
|
||||||
actions: ["iptables-multiport"],
|
actions: ["iptables-multiport"],
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* `backend/app/models/ban.py` — dashboard dashboard sections.
|
* `backend/app/models/ban.py` — dashboard dashboard sections.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { PaginatedListResponse } from "./response";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Time-range selector
|
// Time-range selector
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -57,16 +59,7 @@ export interface DashboardBanItem {
|
|||||||
*
|
*
|
||||||
* Mirrors `DashboardBanListResponse` from `backend/app/models/ban.py`.
|
* Mirrors `DashboardBanListResponse` from `backend/app/models/ban.py`.
|
||||||
*/
|
*/
|
||||||
export interface DashboardBanListResponse {
|
export interface DashboardBanListResponse extends PaginatedListResponse<DashboardBanItem> {}
|
||||||
/** Ban items for the current page. */
|
|
||||||
items: DashboardBanItem[];
|
|
||||||
/** Total number of bans in the selected time window. */
|
|
||||||
total: number;
|
|
||||||
/** Current 1-based page number. */
|
|
||||||
page: number;
|
|
||||||
/** Maximum items per page. */
|
|
||||||
page_size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Ban trend
|
// Ban trend
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
* TypeScript interfaces for the configuration and server settings API.
|
* TypeScript interfaces for the configuration and server settings API.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { CollectionResponse } from "./response";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Ban-time escalation
|
// Ban-time escalation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -66,10 +68,7 @@ export interface JailConfigResponse {
|
|||||||
jail: JailConfig;
|
jail: JailConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JailConfigListResponse {
|
export interface JailConfigListResponse extends CollectionResponse<JailConfig> {}
|
||||||
items: JailConfig[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JailConfigUpdate {
|
export interface JailConfigUpdate {
|
||||||
ban_time?: number | null;
|
ban_time?: number | null;
|
||||||
@@ -535,10 +534,7 @@ export interface InactiveJail {
|
|||||||
has_local_override: boolean;
|
has_local_override: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InactiveJailListResponse {
|
export interface InactiveJailListResponse extends CollectionResponse<InactiveJail> {}
|
||||||
items: InactiveJail[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional override values when activating an inactive jail.
|
* Optional override values when activating an inactive jail.
|
||||||
|
|||||||
@@ -7,7 +7,16 @@
|
|||||||
* - `backend/app/models/geo.py` (GeoDetail / IpLookupResponse)
|
* - `backend/app/models/geo.py` (GeoDetail / IpLookupResponse)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BantimeEscalation, BackendType, LogEncoding } from "./config";
|
import type {
|
||||||
|
BantimeEscalation,
|
||||||
|
BackendType,
|
||||||
|
LogEncoding,
|
||||||
|
} from "./config";
|
||||||
|
import type {
|
||||||
|
CollectionResponse,
|
||||||
|
PaginatedListResponse,
|
||||||
|
CommandResponse,
|
||||||
|
} from "./response";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Jail statistics
|
// Jail statistics
|
||||||
@@ -64,12 +73,14 @@ export interface JailSummary {
|
|||||||
*
|
*
|
||||||
* Mirrors `JailListResponse` from `backend/app/models/jail.py`.
|
* Mirrors `JailListResponse` from `backend/app/models/jail.py`.
|
||||||
*/
|
*/
|
||||||
export interface JailListResponse {
|
export interface JailListResponse extends CollectionResponse<JailSummary> {}
|
||||||
/** All known jails. */
|
|
||||||
items: JailSummary[];
|
/**
|
||||||
/** Total number of jails. */
|
* Response from `GET /api/jails/{name}/ignoreip`.
|
||||||
total: number;
|
*
|
||||||
}
|
* Mirrors `IgnoreListResponse` from `backend/app/models/jail.py`.
|
||||||
|
*/
|
||||||
|
export interface IgnoreListResponse extends CollectionResponse<string> {}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Jail detail
|
// Jail detail
|
||||||
@@ -83,6 +94,8 @@ export interface JailListResponse {
|
|||||||
export interface Jail {
|
export interface Jail {
|
||||||
/** Machine-readable jail name. */
|
/** Machine-readable jail name. */
|
||||||
name: string;
|
name: string;
|
||||||
|
/** Whether the jail is enabled in the configuration. */
|
||||||
|
enabled: boolean;
|
||||||
/** Whether the jail is running. */
|
/** Whether the jail is running. */
|
||||||
running: boolean;
|
running: boolean;
|
||||||
/** Whether the jail is in idle mode. */
|
/** Whether the jail is in idle mode. */
|
||||||
@@ -95,8 +108,10 @@ export interface Jail {
|
|||||||
fail_regex: string[];
|
fail_regex: string[];
|
||||||
/** Ignore-regex patterns used to whitelist log lines. */
|
/** Ignore-regex patterns used to whitelist log lines. */
|
||||||
ignore_regex: string[];
|
ignore_regex: string[];
|
||||||
/** Date-pattern used for timestamp parsing, or empty string. */
|
/** IP addresses and networks explicitly ignored by the jail. */
|
||||||
date_pattern: string;
|
ignore_ips: string[];
|
||||||
|
/** Date-pattern used for timestamp parsing, or `null` when unset. */
|
||||||
|
date_pattern: string | null;
|
||||||
/** Log file encoding (e.g. `"UTF-8"`). */
|
/** Log file encoding (e.g. `"UTF-8"`). */
|
||||||
log_encoding: LogEncoding;
|
log_encoding: LogEncoding;
|
||||||
/** Action names attached to this jail. */
|
/** Action names attached to this jail. */
|
||||||
@@ -136,9 +151,7 @@ export interface JailDetailResponse {
|
|||||||
*
|
*
|
||||||
* Mirrors `JailCommandResponse` from `backend/app/models/jail.py`.
|
* Mirrors `JailCommandResponse` from `backend/app/models/jail.py`.
|
||||||
*/
|
*/
|
||||||
export interface JailCommandResponse {
|
export interface JailCommandResponse extends CommandResponse {
|
||||||
/** Human-readable result message. */
|
|
||||||
message: string;
|
|
||||||
/** Target jail name, or `"*"` for operations on all jails. */
|
/** Target jail name, or `"*"` for operations on all jails. */
|
||||||
jail: string;
|
jail: string;
|
||||||
}
|
}
|
||||||
@@ -172,12 +185,7 @@ export interface ActiveBan {
|
|||||||
*
|
*
|
||||||
* Mirrors `ActiveBanListResponse` from `backend/app/models/ban.py`.
|
* Mirrors `ActiveBanListResponse` from `backend/app/models/ban.py`.
|
||||||
*/
|
*/
|
||||||
export interface ActiveBanListResponse {
|
export interface ActiveBanListResponse extends CollectionResponse<ActiveBan> {}
|
||||||
/** List of all currently active bans. */
|
|
||||||
items: ActiveBan[];
|
|
||||||
/** Total number of active bans. */
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Geo / IP lookup
|
// Geo / IP lookup
|
||||||
@@ -207,16 +215,7 @@ export interface UnbanAllResponse {
|
|||||||
*
|
*
|
||||||
* Mirrors `JailBannedIpsResponse` from `backend/app/models/ban.py`.
|
* Mirrors `JailBannedIpsResponse` from `backend/app/models/ban.py`.
|
||||||
*/
|
*/
|
||||||
export interface JailBannedIpsResponse {
|
export interface JailBannedIpsResponse extends PaginatedListResponse<ActiveBan> {}
|
||||||
/** Active ban entries for the current page. */
|
|
||||||
items: ActiveBan[];
|
|
||||||
/** Total matching entries (after applying any search filter). */
|
|
||||||
total: number;
|
|
||||||
/** Current page number (1-based). */
|
|
||||||
page: number;
|
|
||||||
/** Number of items per page. */
|
|
||||||
page_size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeoDetail {
|
export interface GeoDetail {
|
||||||
/** ISO 3166-1 alpha-2 country code (e.g. `"DE"`), or `null`. */
|
/** ISO 3166-1 alpha-2 country code (e.g. `"DE"`), or `null`. */
|
||||||
|
|||||||
27
frontend/src/types/response.ts
Normal file
27
frontend/src/types/response.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Standardized API response envelope types.
|
||||||
|
*
|
||||||
|
* These wrappers mirror the backend response envelope contract defined in
|
||||||
|
* `backend/app/models/response.py`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CollectionResponse<T> {
|
||||||
|
/** Data items in the collection. */
|
||||||
|
items: T[];
|
||||||
|
/** Total number of items in the collection. */
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedListResponse<T> extends CollectionResponse<T> {
|
||||||
|
/** Current page number (1-based). */
|
||||||
|
page: number;
|
||||||
|
/** Number of items per page. */
|
||||||
|
page_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandResponse {
|
||||||
|
/** Human-readable result message. */
|
||||||
|
message: string;
|
||||||
|
/** Whether the command succeeded. */
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user