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>
147 lines
4.4 KiB
Python
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
|