Phase 1 (Foundation): - Project restructure (presenton-main → backend/ + frontend/) - Database schema (8 new models, Alembic config, seed script) - Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware) - RBAC (access_service, rbac_middleware, admin routers) - Audit logging (fire-and-forget, AuditMiddleware, admin router) - i18n (react-i18next with 5 namespace files) Phase 2 (Admin Panel & Client Management): - Admin panel shell (sidebar layout, role guard, 12 pages) - Redux admin slice with 18 async thunks - User management (role changes, deactivation) - Client management (CRUD, brand config, team management) - Brand config editor (colors, fonts, logos, voice rules) - Master deck upload & parser (PPTX → HTML → React pipeline) - Audit log viewer with filters and CSV/JSON export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
62 lines
2 KiB
Python
62 lines
2 KiB
Python
"""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
|