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