ppt-tool/backend/api/v1/auth/router.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

134 lines
3.9 KiB
Python

import uuid
from fastapi import APIRouter, Depends, HTTPException, Response, Request
from fastapi.responses import RedirectResponse, JSONResponse
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from services.database import get_async_session
from services.auth_service import AuthService
AUTH_ROUTER = APIRouter(prefix="/api/v1/auth", tags=["Auth"])
auth_service = AuthService()
class DevLoginRequest(BaseModel):
email: str
password: str
@AUTH_ROUTER.get("/dev-status")
async def dev_status():
"""Check if dev auth mode is enabled."""
return {"dev_mode": auth_service.is_dev_mode}
@AUTH_ROUTER.get("/login")
async def login():
"""Redirect to Azure AD login, or return dev mode info."""
if auth_service.is_dev_mode:
return JSONResponse(
status_code=200,
content={
"dev_mode": True,
"message": "Use POST /api/v1/auth/dev-login with email and password",
},
)
try:
url = auth_service.get_authorization_url()
return RedirectResponse(url=url)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to generate login URL: {e}")
@AUTH_ROUTER.get("/callback")
async def callback(
code: str = "",
error: str = "",
error_description: str = "",
session: AsyncSession = Depends(get_async_session),
):
"""Azure AD OAuth callback."""
if error:
raise HTTPException(status_code=401, detail=error_description or error)
if not code:
raise HTTPException(status_code=400, detail="Missing authorization code")
try:
result = await auth_service.exchange_code_for_token(code)
claims = result.get("id_token_claims", {})
user = await auth_service.get_or_create_user(claims, session)
token = auth_service.create_session_jwt(user)
response = RedirectResponse(url="/upload", status_code=302)
response.set_cookie(
key="session_token",
value=token,
httponly=True,
secure=False, # Set True in production with HTTPS
samesite="lax",
max_age=86400, # 24 hours
)
return response
except ValueError as e:
raise HTTPException(status_code=401, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Authentication failed: {e}")
@AUTH_ROUTER.post("/dev-login")
async def dev_login(
body: DevLoginRequest,
session: AsyncSession = Depends(get_async_session),
):
"""Dev-mode login with email and password. Only available when Azure AD is not configured."""
if not auth_service.is_dev_mode:
raise HTTPException(status_code=404, detail="Dev login not available")
user = await auth_service.dev_login(body.email, body.password, session)
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
token = auth_service.create_session_jwt(user)
response = JSONResponse(
content={
"id": str(user.id),
"email": user.email,
"display_name": user.display_name,
"role": user.role,
}
)
response.set_cookie(
key="session_token",
value=token,
httponly=True,
secure=False,
samesite="lax",
max_age=86400,
)
return response
@AUTH_ROUTER.get("/me")
async def get_current_user_info(request: Request):
"""Return current authenticated user info."""
user = getattr(request.state, "user", None)
if not user:
raise HTTPException(status_code=401, detail="Not authenticated")
return {
"id": str(user.id),
"email": user.email,
"displayName": user.display_name,
"role": user.role,
}
@AUTH_ROUTER.post("/logout")
async def logout():
"""Clear session cookie."""
response = JSONResponse(content={"message": "Logged out"})
response.delete_cookie("session_token")
return response