Files
ThinkStorm/thinkstorm/api/auth_routes.py
T

162 lines
5.8 KiB
Python

"""
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