"""Auth router: login, google OAuth, and current user info.""" from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.auth import ( create_access_token, get_current_user, verify_google_id_token, ) from app.config import settings from app.database import get_session from app.user_models import User router = APIRouter() # ── Request/Response schemas ───────────────────────────────── class LoginRequest(BaseModel): username: str password: str class GoogleLoginRequest(BaseModel): id_token: str class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" class UserInfoResponse(BaseModel): id: int email: str display_name: str avatar_url: str | None auth_provider: str is_admin: bool model_config = {"from_attributes": True} # ── Routes ─────────────────────────────────────────────────── @router.post("/login", response_model=TokenResponse) async def login(payload: LoginRequest): """Bootstrap admin login (plaintext credentials from env vars).""" if not settings.ADMIN_USERNAME or not settings.ADMIN_PASSWORD: raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Bootstrap admin not configured") if payload.username != settings.ADMIN_USERNAME or payload.password != settings.ADMIN_PASSWORD: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") # This is handled in the auth router to keep it self-contained from app.database import async_session async with async_session() as session: # Find or create the bootstrap admin user result = await session.execute( select(User).where( User.email == f"{settings.ADMIN_USERNAME}@local", User.auth_provider == "local", ) ) user = result.scalar_one_or_none() if user is None: user = User( email=f"{settings.ADMIN_USERNAME}@local", display_name=settings.ADMIN_USERNAME, auth_provider="local", is_admin=True, ) session.add(user) await session.commit() await session.refresh(user) token = create_access_token(user.id, user.is_admin) return TokenResponse(access_token=token) @router.post("/google", response_model=TokenResponse) async def login_with_google(payload: GoogleLoginRequest, session: AsyncSession = Depends(get_session)): """Google OAuth sign-in. Accepts an ID token, verifies it, upserts user, returns JWT.""" info = await verify_google_id_token(payload.id_token) result = await session.execute(select(User).where(User.email == info["email"])) user = result.scalar_one_or_none() if user is None: user = User( email=info["email"], display_name=info["name"], avatar_url=info["picture"] or None, auth_provider="google", is_admin=False, ) session.add(user) await session.commit() await session.refresh(user) token = create_access_token(user.id, user.is_admin) return TokenResponse(access_token=token) @router.get("/me", response_model=UserInfoResponse) async def me(current_user: User = Depends(get_current_user)): """Return current user info.""" return current_user