""" ThinkStorm Authentication API Router Handles login, logout, current user session status, and Gitea OAuth2 flow. """ import json 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" data = urllib.parse.urlencode({ "client_id": client_id, "client_secret": client_secret, "code": code, "grant_type": "authorization_code", "redirect_uri": redirect_uri }).encode("utf-8") req = urllib.request.Request( token_url, data=data, headers={ "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "User-Agent": "ThinkStorm-Orchestrator/0.1" }, method="POST" ) with urllib.request.urlopen(req, timeout=10.0) as resp: resp_bytes = resp.read() try: token_data = json.loads(resp_bytes.decode("utf-8")) except Exception: token_data = urllib.parse.parse_qs(resp_bytes.decode("utf-8")) 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) except Exception as e: print(f"[Gitea OAuth] Token exchange error: {e}") # Fallback to default Gitea session if exchange failed if not user_info: user_info = {"id": 1001, "login": "gitea-user", "is_admin": False} 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