ppt-tool/backend/api/v1/auth/router.py
Vadym Samoilenko d3d1667a79 Phase 2: Admin panel, analytics, storage, template pipeline, multi-provider LLM
- Fix admin sidebar: remove duplicate Teams, add Storage nav item
- Analytics: client-scoped queries, super_admin sees all (including NULL client_id)
- Storage management: list/download/delete presentations with file metadata
- Settings page with brand config router
- AI usage tracking: new AIUsageModel, ai_usage_service, analytics endpoint
- Master deck → template bridge: _register_as_template creates TemplateModel
  + PresentationLayoutCodeModel so parsed layouts appear in template picker
- Multi-provider LLM vision in parser: Anthropic/Google/OpenAI with asyncio.to_thread
- Fix PPTX upload 400: accept by .pptx extension (browser sends octet-stream)
- Fix reparse FK violation: presentation_id=None for parse_master_deck jobs
- Worker job_timeout increased to 1800s for LLM-heavy master deck parsing
- PYTHONUNBUFFERED=1 in docker-compose worker for real-time log output
- Auth: clientId in /me response, dev-login cookie improvements
- Frontend: auth slice clientId, master-deck thumbnails, storage page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:39:34 +00:00

143 lines
4.2 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="/dashboard", 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,
session: AsyncSession = Depends(get_async_session),
):
"""Return current authenticated user info."""
user = getattr(request.state, "user", None)
if not user:
raise HTTPException(status_code=401, detail="Not authenticated")
from services.access_service import get_accessible_client_ids
client_ids = await get_accessible_client_ids(user, session)
primary_client_id = str(client_ids[0]) if client_ids else None
return {
"id": str(user.id),
"email": user.email,
"displayName": user.display_name,
"role": user.role,
"clientId": primary_client_id,
}
@AUTH_ROUTER.post("/logout")
async def logout():
"""Clear session cookie."""
response = JSONResponse(content={"message": "Logged out"})
response.delete_cookie("session_token")
return response