mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-21 21:13:31 +01:00
MAESTRO: add health endpoint
This commit is contained in:
@@ -32,7 +32,7 @@ This phase protects the analysis service from abuse with per-IP rate limiting an
|
||||
- [x] T041 [US6] Create `backend/app/middleware/rate_limiter.py` — per-IP sliding window rate limiter (async-safe in-memory). Configurable limit via `config.py`. Returns 429 with Retry-After header and CAPTCHA challenge token (NFR-11, NFR-12). Note: per-instance counters — acceptable for initial release; shared store upgradeable later. Verify `test_rate_limiter.py` passes (TDD Green)
|
||||
- [x] T042 [US6] Create `backend/app/routers/captcha.py` — `POST /api/captcha/verify` endpoint. Server-generated visual noise CAPTCHA (randomly distorted text). Returns HMAC-signed bypass token (5-minute expiry) on success. Token exempts IP from rate limiting. Response schema in `backend/app/schemas/captcha.py`. Verify `test_captcha.py` passes (TDD Green)
|
||||
- [x] T043 [P] [US6] Create `frontend/src/components/CaptchaChallenge.tsx` — modal on 429 response. Displays CAPTCHA image, on verification stores bypass token and retries original request. FontAwesome lock/unlock icons. Keyboard accessible (NFR-02). Verify `CaptchaChallenge.test.tsx` passes (TDD Green)
|
||||
- [ ] T044 [US6] Create `backend/app/schemas/health.py` and `backend/app/routers/health.py` — `GET /api/health` returning status (up/degraded/down), version, uptime, scanner count (NFR-15). Verify `test_health.py` passes (TDD Green)
|
||||
- [x] T044 [US6] Create `backend/app/schemas/health.py` and `backend/app/routers/health.py` — `GET /api/health` returning status (up/degraded/down), version, uptime, scanner count (NFR-15). Verify `test_health.py` passes (TDD Green)
|
||||
- [ ] T045 [US6] Register all routers and middleware in `backend/app/main.py` — CORS middleware (frontend origin), rate limiter, routers (analysis, tests, health, captcha). Verify stateless operation (NFR-16). Note: rate limiter per-instance state is accepted trade-off (see T041)
|
||||
|
||||
## Completion
|
||||
|
||||
@@ -4,6 +4,7 @@ from app.core.config import get_settings
|
||||
from app.middleware.rate_limiter import RateLimiterMiddleware, SlidingWindowRateLimiter
|
||||
from app.routers.analysis import router as analysis_router
|
||||
from app.routers.captcha import router as captcha_router
|
||||
from app.routers.health import router as health_router
|
||||
from app.routers.tests import router as tests_router
|
||||
|
||||
app = FastAPI(title="Web Header Analyzer API")
|
||||
@@ -16,6 +17,7 @@ app.add_middleware(
|
||||
)
|
||||
app.include_router(analysis_router)
|
||||
app.include_router(captcha_router)
|
||||
app.include_router(health_router)
|
||||
app.include_router(tests_router)
|
||||
|
||||
|
||||
|
||||
75
backend/app/routers/health.py
Normal file
75
backend/app/routers/health.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import tomllib
|
||||
from importlib import metadata
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.engine.scanner_registry import ScannerRegistry
|
||||
from app.schemas.health import HealthResponse
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["health"])
|
||||
|
||||
_START_TIME = time.monotonic()
|
||||
_PROJECT_NAME = "web-header-analyzer-backend"
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
def health_check() -> HealthResponse:
|
||||
status = "up"
|
||||
scanner_count = 0
|
||||
|
||||
try:
|
||||
registry = ScannerRegistry()
|
||||
scanner_count = len(registry.list_tests())
|
||||
if scanner_count == 0:
|
||||
status = "degraded"
|
||||
except Exception:
|
||||
status = "down"
|
||||
scanner_count = 0
|
||||
|
||||
return HealthResponse(
|
||||
status=status,
|
||||
version=_resolve_version(),
|
||||
uptime=_uptime_seconds(),
|
||||
scanner_count=scanner_count,
|
||||
)
|
||||
|
||||
|
||||
def _uptime_seconds() -> float:
|
||||
return max(0.0, time.monotonic() - _START_TIME)
|
||||
|
||||
|
||||
def _resolve_version() -> str:
|
||||
env_version = os.getenv("WHA_VERSION", "").strip()
|
||||
if env_version:
|
||||
return env_version
|
||||
|
||||
try:
|
||||
return metadata.version(_PROJECT_NAME)
|
||||
except metadata.PackageNotFoundError:
|
||||
pass
|
||||
|
||||
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
||||
if pyproject_path.exists():
|
||||
try:
|
||||
data = pyproject_path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
data = ""
|
||||
if data:
|
||||
try:
|
||||
parsed = tomllib.loads(data)
|
||||
except ValueError:
|
||||
parsed = {}
|
||||
version = (
|
||||
parsed.get("project", {}).get("version")
|
||||
if isinstance(parsed, dict)
|
||||
else None
|
||||
)
|
||||
if isinstance(version, str) and version.strip():
|
||||
return version.strip()
|
||||
|
||||
return "unknown"
|
||||
17
backend/app/schemas/health.py
Normal file
17
backend/app/schemas/health.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
status: Literal["up", "degraded", "down"]
|
||||
version: str
|
||||
uptime: float
|
||||
scanner_count: int = Field(alias="scannerCount")
|
||||
|
||||
|
||||
__all__ = ["HealthResponse"]
|
||||
Reference in New Issue
Block a user