Capture ThinkStorm project: codebase state, workflows, and access control policies
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user