"""Auth router — in-page login against Casdoor (password grant). POST /auth/login: the frontend sends username/password; this router exchanges them at Casdoor's OAuth token endpoint (grant_type=password), verifies the issued JWT locally (JWKS), auto-provisions the `users` row and returns the token + profile (role resolved from users.role). No browser redirect. Error mapping (documented in docs/casdoor-auth.md): - wrong credentials -> 401 UNAUTHORIZED - Casdoor unreachable -> 503 UPSTREAM_UNAVAILABLE - Casdoor misconfigured -> 503 UPSTREAM_UNAVAILABLE (details in api logs) """ import logging import httpx from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.auth import _verify_token, context_from_claims from app.config import settings from app.db.engine import get_db from app.routers.users import _user_projects, user_to_dict from app.utils.errors import ServiceUnavailableError, UnauthorizedError logger = logging.getLogger("procurement.auth") router = APIRouter(prefix="/auth", tags=["auth"]) class LoginRequest(BaseModel): username: str password: str @router.post("/login") async def login(payload: LoginRequest, db: AsyncSession = Depends(get_db)): """Exchange username/password for a Casdoor JWT (OAuth password grant).""" url = f"{settings.CASDOOR_ENDPOINT}/api/login/oauth/access_token" form = { "grant_type": "password", "client_id": settings.CASDOOR_CLIENT_ID, "client_secret": settings.CASDOOR_CLIENT_SECRET, "username": payload.username.strip(), "password": payload.password, "scope": "openid profile email", } try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.post(url, data=form) except httpx.HTTPError as exc: logger.error("Casdoor token endpoint unreachable (%s): %s", url, exc) raise ServiceUnavailableError("سرویس احراز هویت (Casdoor) در دسترس نیست") from exc try: body = resp.json() except ValueError as exc: logger.error("Casdoor token endpoint returned non-JSON (status %s)", resp.status_code) raise ServiceUnavailableError("پاسخ نامعتبر از سرویس احراز هویت") from exc access_token = body.get("access_token") if not access_token: err = body.get("error") or "unknown_error" if err in ("invalid_username_or_password", "invalid_grant"): raise UnauthorizedError("نام کاربری یا رمز عبور نادرست است", field="password") logger.error( "Casdoor rejected password grant: error=%s description=%s", err, body.get("error_description", ""), ) raise ServiceUnavailableError("سرویس احراز هویت به درستی پیکربندی نشده است") # Verify the freshly issued token with our own JWKS checks, then resolve # the app-side user (auto-provision on first login). claims = await _verify_token(access_token) ctx = await context_from_claims(claims, db) projects = await _user_projects(db, ctx.user) return { "data": { "access_token": access_token, "token_type": "bearer", "expires_in": body.get("expires_in"), "user": user_to_dict(ctx.user, projects), } }