feat(auth): add AuthMiddleware with JWT parsing and in-memory rate limiting; wire into app; add tests and docs

This commit is contained in:
2025-10-13 00:18:46 +02:00
parent bf5d80bbb3
commit 9096afbace
6 changed files with 179 additions and 22 deletions

View File

@@ -5,7 +5,9 @@ from fastapi.security import HTTPAuthorizationCredentials
from src.server.models.auth import AuthStatus, LoginRequest, LoginResponse, SetupRequest
from src.server.services.auth_service import AuthError, LockedOutError, auth_service
from src.server.utils.dependencies import optional_auth, security
# NOTE: import dependencies (optional_auth, security) lazily inside handlers
# to avoid importing heavyweight modules (e.g. sqlalchemy) at import time.
router = APIRouter(prefix="/api/auth", tags=["auth"])
@@ -48,15 +50,35 @@ def login(req: LoginRequest):
@router.post("/logout")
def logout(credentials: HTTPAuthorizationCredentials = Depends(security)):
def logout(credentials: HTTPAuthorizationCredentials = None):
"""Logout by revoking token (no-op for stateless JWT)."""
token = credentials.credentials
# Import security dependency lazily to avoid heavy imports during test
if credentials is None:
from fastapi import Depends
from src.server.utils.dependencies import security as _security
# Trigger dependency resolution during normal request handling
credentials = Depends(_security)
# If a plain credentials object was provided, extract token
token = getattr(credentials, "credentials", None)
# Placeholder; auth_service.revoke_token can be expanded to persist revocations
auth_service.revoke_token(token)
return {"status": "ok"}
@router.get("/status", response_model=AuthStatus)
def status(auth: Optional[dict] = Depends(optional_auth)):
def status(auth: Optional[dict] = None):
"""Return whether master password is configured and if caller is authenticated."""
# Lazy import to avoid pulling in database/sqlalchemy during module import
from fastapi import Depends
try:
from src.server.utils.dependencies import optional_auth as _optional_auth
except Exception:
_optional_auth = None
# If dependency injection didn't provide auth, attempt to resolve optionally
if auth is None and _optional_auth is not None:
auth = Depends(_optional_auth)
return AuthStatus(configured=auth_service.is_configured(), authenticated=bool(auth))