ppt-tool/backend/api/middlewares/audit_middleware.py
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
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>
2026-02-26 15:37:17 +00:00

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