"""Middleware that auto-logs mutating API requests to audit log.""" from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response from services import audit_service AUDITABLE_METHODS = {"POST", "PUT", "PATCH", "DELETE"} def _extract_resource_type(path: str) -> str: """Extract resource type from URL path. e.g. /api/v1/admin/users/123 -> users /api/v1/ppt/presentation/create -> presentation """ parts = [p for p in path.split("/") if p] # Walk backwards to find first meaningful segment (skip IDs and actions) for part in reversed(parts): if part in ("v1", "api", "admin", "ppt"): continue # Skip UUID-looking segments if len(part) == 36 and part.count("-") == 4: continue # Skip common action words if part in ("create", "update", "delete", "export", "login", "logout", "callback"): continue return part return "unknown" class AuditMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next) -> Response: response = await call_next(request) # Only log mutating requests to API endpoints if request.method not in AUDITABLE_METHODS: return response if not request.url.path.startswith("/api/"): return response # Skip auth endpoints (logged separately) if "/auth/" in request.url.path: return response # Only log successful mutations if response.status_code >= 400: return response user = getattr(request.state, "user", None) user_id = user.id if user else None ip_address = request.client.host if request.client else None resource_type = _extract_resource_type(request.url.path) audit_service.log( user_id=user_id, action=f"{request.method} {request.url.path}", resource_type=resource_type, ip_address=ip_address, ) return response