Improve docs and security defaults

This commit is contained in:
2025-10-22 15:22:58 +02:00
parent ebb0769ed4
commit 92795cf9b3
16 changed files with 283 additions and 180 deletions

View File

@@ -1,4 +1,10 @@
"""Maintenance and system management API endpoints."""
"""Maintenance API endpoints for system housekeeping and diagnostics.
This module exposes cleanup routines, system statistics, maintenance
operations, and health reporting endpoints that rely on the shared system
utilities and monitoring services. The routes allow administrators to
prune logs, inspect disk usage, vacuum or analyze the database, and gather
holistic health metrics for AniWorld deployments."""
import logging
from typing import Any, Dict

View File

@@ -44,22 +44,15 @@ app = FastAPI(
redoc_url="/api/redoc"
)
# Configure CORS
# WARNING: In production, ensure CORS_ORIGINS is properly configured
# Default to localhost for development, configure via environment variable
cors_origins = (
settings.cors_origins.split(",")
if settings.cors_origins and settings.cors_origins != "*"
else (
["http://localhost:3000", "http://localhost:8000"]
if settings.cors_origins == "*"
else []
)
)
# Configure CORS using environment-driven configuration.
allowed_origins = settings.allowed_origins or [
"http://localhost:3000",
"http://localhost:8000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins if cors_origins else ["*"],
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

View File

@@ -46,21 +46,33 @@ class AuthMiddleware(BaseHTTPMiddleware):
path = request.url.path or ""
# Apply rate limiting to auth endpoints that accept credentials
if path in ("/api/auth/login", "/api/auth/setup") and request.method.upper() == "POST":
if (
path in ("/api/auth/login", "/api/auth/setup")
and request.method.upper() == "POST"
):
client_host = self._get_client_ip(request)
rec = self._rate.setdefault(client_host, {"count": 0, "window_start": time.time()})
rate_limit_record = self._rate.setdefault(
client_host,
{"count": 0, "window_start": time.time()},
)
now = time.time()
if now - rec["window_start"] > self.window_seconds:
# reset window
rec["window_start"] = now
rec["count"] = 0
# The limiter uses a fixed window; once the window expires, we
# reset the counter for that client and start measuring again.
if now - rate_limit_record["window_start"] > self.window_seconds:
rate_limit_record["window_start"] = now
rate_limit_record["count"] = 0
rec["count"] += 1
if rec["count"] > self.rate_limit_per_minute:
rate_limit_record["count"] += 1
if rate_limit_record["count"] > self.rate_limit_per_minute:
# Too many requests in window — return a JSON 429 response
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={"detail": "Too many authentication attempts, try again later"},
content={
"detail": (
"Too many authentication attempts, "
"try again later"
)
},
)
# If Authorization header present try to decode token and attach session

View File

@@ -228,7 +228,9 @@ class DownloadService:
added_at=datetime.now(timezone.utc),
)
# Insert based on priority
# Insert based on priority. High-priority downloads jump the
# line via appendleft so they execute before existing work;
# everything else is appended to preserve FIFO order.
if priority == DownloadPriority.HIGH:
self._pending_queue.appendleft(item)
else:

View File

@@ -5,9 +5,13 @@ This module provides dependency injection functions for the FastAPI
application, including SeriesApp instances, AnimeService, DownloadService,
database sessions, and authentication dependencies.
"""
from typing import TYPE_CHECKING, AsyncGenerator, Optional
import logging
import time
from asyncio import Lock
from dataclasses import dataclass
from typing import TYPE_CHECKING, AsyncGenerator, Dict, Optional
from fastapi import Depends, HTTPException, status
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
try:
@@ -19,13 +23,15 @@ from src.config.settings import settings
from src.core.SeriesApp import SeriesApp
from src.server.services.auth_service import AuthError, auth_service
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from src.server.services.anime_service import AnimeService
from src.server.services.download_service import DownloadService
# Security scheme for JWT authentication
# Use auto_error=False to handle errors manually and return 401 instead of 403
security = HTTPBearer(auto_error=False)
http_bearer_security = HTTPBearer(auto_error=False)
# Global SeriesApp instance
@@ -36,6 +42,19 @@ _anime_service: Optional["AnimeService"] = None
_download_service: Optional["DownloadService"] = None
@dataclass
class RateLimitRecord:
"""Track request counts within a fixed time window."""
count: int
window_start: float
_RATE_LIMIT_BUCKETS: Dict[str, RateLimitRecord] = {}
_rate_limit_lock = Lock()
_RATE_LIMIT_WINDOW_SECONDS = 60.0
def get_series_app() -> SeriesApp:
"""
Dependency to get SeriesApp instance.
@@ -104,7 +123,9 @@ async def get_database_session() -> AsyncGenerator:
def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
http_bearer_security
),
) -> dict:
"""
Dependency to get current authenticated user.
@@ -195,7 +216,7 @@ def get_current_user_optional(
class CommonQueryParams:
"""Common query parameters for API endpoints."""
"""Reusable pagination parameters shared across API endpoints."""
def __init__(self, skip: int = 0, limit: int = 100) -> None:
"""Create a reusable pagination parameter container.
@@ -226,23 +247,47 @@ def common_parameters(
# Dependency for rate limiting (placeholder)
async def rate_limit_dependency():
"""
Dependency for rate limiting API requests.
TODO: Implement rate limiting logic
"""
pass
async def rate_limit_dependency(request: Request) -> None:
"""Apply a simple fixed-window rate limit to incoming requests."""
client_id = "unknown"
if request.client and request.client.host:
client_id = request.client.host
max_requests = max(1, settings.api_rate_limit)
now = time.time()
async with _rate_limit_lock:
record = _RATE_LIMIT_BUCKETS.get(client_id)
if not record or now - record.window_start >= _RATE_LIMIT_WINDOW_SECONDS:
_RATE_LIMIT_BUCKETS[client_id] = RateLimitRecord(
count=1,
window_start=now,
)
return
record.count += 1
if record.count > max_requests:
logger.warning("Rate limit exceeded", extra={"client": client_id})
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many requests. Please slow down.",
)
# Dependency for request logging (placeholder)
async def log_request_dependency():
"""
Dependency for logging API requests.
TODO: Implement request logging logic
"""
pass
async def log_request_dependency(request: Request) -> None:
"""Log request metadata for auditing and debugging purposes."""
logger.info(
"API request",
extra={
"method": request.method,
"path": request.url.path,
"client": request.client.host if request.client else "unknown",
"query": dict(request.query_params),
},
)
def get_anime_service() -> "AnimeService":

View File

@@ -251,8 +251,12 @@ class SystemUtilities:
info = SystemUtilities.get_process_info(proc.pid)
if info:
processes.append(info)
except Exception:
pass
except Exception as process_error:
logger.debug(
"Skipping process %s: %s",
proc.pid,
process_error,
)
return processes
except Exception as e: