ppt-tool/backend/api/v1/auth/router.py
Vadym Samoilenko c431d4ab45 Implement critical security fixes and modern design system (Pre-launch P0 tasks)
Security Improvements (P0.0-P0.4):
- P0.0: Migrate to Gemini-only AI stack (simplified, single billing)
- P0.1: Fix CORS to restrict allowed origins from env (was *)
- P0.2: Remove hardcoded dev password, require env var
- P0.3: Add rate limiting (slowapi) - 3-10 req/min on sensitive endpoints
- P0.4: Add request size limits (100MB default via middleware)

New Features:
- Unified LLM service with Google Gemini priority
- OXML geometry extractor for layout parsing
- TSX validator for generated React components
- Client ID support in presentation requests with access control
- Configurable LLM/image timeouts via env vars

Modern Design System (P0.9 - partial):
- Enhanced CSS design tokens (primary, semantic colors, shadows)
- Typography scale (h1-h4, body variants, caption)
- Modern animations (fadeIn, slideIn, scaleIn)
- Updated Button component with better variants and hover effects
- Created unified Card and StatusBadge components
- Applied design system to Dashboard and Settings pages

Backend Improvements:
- Master deck parser simplification
- Slide-to-HTML endpoint cleanup (325 lines removed)
- Better error handling in prompts endpoint

Frontend Improvements:
- Settings UI simplified to show only Google/Gemini
- Dashboard uses CSS variables instead of hardcoded colors
- Improved button transitions and hover states

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-02-27 18:28:24 +00:00

147 lines
4.4 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
from api.middlewares.rate_limit_middleware import limiter
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")
@limiter.limit("5/minute")
async def login(request: Request):
"""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")
@limiter.limit("3/minute")
async def dev_login(
request: Request,
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