""" ThinkStorm Authentication API Router Handles login, logout, current user session status, and Gitea OAuth2 flow. """ import json import requests import urllib.request import urllib.parse from typing import Optional, Dict, Any from fastapi import APIRouter, Request, Response, HTTPException, status, Depends from fastapi.responses import RedirectResponse from pydantic import BaseModel from ..config import config from ..database import get_db, hash_password, get_utc_now from ..models import User, UserRole from ..auth import ( authenticate_local, create_session, destroy_session, get_current_user, get_or_create_gitea_user ) from ..services.gitea import GiteaAdapter router = APIRouter(prefix="/api/auth", tags=["auth"]) gitea_svc = GiteaAdapter() class LocalLoginRequest(BaseModel): username: str password: str @router.post("/login") async def login_local(payload: LocalLoginRequest, response: Response): """Authenticates local user or admin bootstrap.""" user = authenticate_local(payload.username, payload.password) if not user: raise HTTPException(status_code=401, detail="Invalid username or password.") token = create_session(user.id, user.username, user.role.value) response.set_cookie( key="thinkstorm_session", value=token, httponly=True, samesite="lax", max_age=86400 * 7 ) return { "message": "Login successful.", "token": token, "user": { "id": user.id, "username": user.username, "role": user.role.value } } @router.post("/logout") async def logout(request: Request, response: Response): token = request.cookies.get("thinkstorm_session") if token: destroy_session(token) response.delete_cookie("thinkstorm_session") return {"message": "Logged out successfully."} @router.get("/me") async def get_me(current_user: User = Depends(get_current_user)): return { "id": current_user.id, "username": current_user.username, "role": current_user.role.value, "is_authenticated": current_user.role != UserRole.ANONYMOUS, "is_admin": current_user.role == UserRole.ADMIN } def get_effective_base_url(request: Request) -> str: if config.base_url and config.base_url != "http://localhost:8000": return config.base_url.rstrip("/") proto = request.headers.get("x-forwarded-proto", "http") host = request.headers.get("x-forwarded-host") or request.headers.get("host") if host: return f"{proto}://{host}" return str(request.base_url).rstrip("/") @router.get("/gitea/url") async def get_gitea_oauth_url(request: Request): """Returns Gitea OAuth authorization redirect URL.""" base_url = get_effective_base_url(request) redirect_uri = f"{base_url}/auth/gitea/callback" client_id = config.services.gitea_client_id or "thinkstorm-oauth" auth_url = ( f"{config.services.gitea_url}/login/oauth/authorize?" f"client_id={urllib.parse.quote(client_id)}&" f"redirect_uri={urllib.parse.quote(redirect_uri)}&" f"response_type=code&state=thinkstorm" ) return {"auth_url": auth_url, "redirect_uri": redirect_uri} @router.get("/gitea/callback") async def gitea_oauth_callback(request: Request, code: str = "", error: str = ""): """Exchanges Gitea authorization code for access token and creates user session.""" if error or not code: print(f"[Gitea OAuth] Callback returned error or empty code: {error}") return RedirectResponse(url="/login?error=oauth_failed", status_code=status.HTTP_303_SEE_OTHER) base_url = get_effective_base_url(request) redirect_uri = f"{base_url}/auth/gitea/callback" user_info = None client_id = config.services.gitea_client_id client_secret = config.services.gitea_client_secret if client_secret and client_id: try: token_url = f"{config.services.gitea_url}/login/oauth/access_token" payload = { "client_id": client_id, "client_secret": client_secret, "code": code, "grant_type": "authorization_code", "redirect_uri": redirect_uri } headers = { "Accept": "application/json", "User-Agent": "ThinkStorm-Orchestrator/0.1" } resp = requests.post(token_url, data=payload, headers=headers, timeout=10.0) if resp.status_code == 200: try: token_data = resp.json() except Exception: token_data = urllib.parse.parse_qs(resp.text) token_data = {k: v[0] for k, v in token_data.items()} access_token = token_data.get("access_token") if access_token: user_info = await gitea_svc.verify_oauth_user(access_token) else: print(f"[Gitea OAuth] Token exchange returned status {resp.status_code}: {resp.text[:150]}") except Exception as e: print(f"[Gitea OAuth] Token exchange error: {e}") if not user_info: print("[Gitea OAuth] Could not fetch valid user profile from Gitea OAuth access token.") return RedirectResponse(url="/login?error=oauth_verify_failed", status_code=status.HTTP_303_SEE_OTHER) user = get_or_create_gitea_user(user_info) token = create_session(user.id, user.username, user.role.value) response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER) response.set_cookie( key="thinkstorm_session", value=token, httponly=True, samesite="lax", max_age=86400 * 7 ) return response