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>
82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
import uuid
|
|
|
|
from fastapi import Request
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.responses import JSONResponse
|
|
|
|
from services.auth_service import AuthService
|
|
from services.database import async_session_maker
|
|
from models.sql.user import UserModel
|
|
|
|
# Paths that skip authentication
|
|
PUBLIC_PATH_PREFIXES = [
|
|
"/api/v1/auth/",
|
|
"/docs",
|
|
"/openapi.json",
|
|
"/api/health",
|
|
]
|
|
|
|
auth_service = AuthService()
|
|
|
|
|
|
class AuthMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
path = request.url.path
|
|
|
|
# Skip auth for public paths
|
|
for prefix in PUBLIC_PATH_PREFIXES:
|
|
if path.startswith(prefix):
|
|
request.state.user = None
|
|
return await call_next(request)
|
|
|
|
# Skip auth for non-API paths (Next.js frontend routes)
|
|
if not path.startswith("/api/"):
|
|
request.state.user = None
|
|
return await call_next(request)
|
|
|
|
# Extract token from cookie or Authorization header
|
|
token = request.cookies.get("session_token")
|
|
if not token:
|
|
auth_header = request.headers.get("Authorization")
|
|
if auth_header and auth_header.startswith("Bearer "):
|
|
token = auth_header[7:]
|
|
|
|
if not token:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Authentication required"},
|
|
)
|
|
|
|
# Validate JWT
|
|
claims = auth_service.validate_token(token)
|
|
if not claims:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Invalid or expired token"},
|
|
)
|
|
|
|
# Load user from DB
|
|
try:
|
|
user_id = uuid.UUID(claims["sub"])
|
|
async with async_session_maker() as session:
|
|
user = await session.get(UserModel, user_id)
|
|
except (ValueError, KeyError):
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Invalid token payload"},
|
|
)
|
|
|
|
if not user:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "User not found"},
|
|
)
|
|
|
|
if not user.is_active:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Account deactivated"},
|
|
)
|
|
|
|
request.state.user = user
|
|
return await call_next(request)
|