Files
xAdmin 41e08611c9 Implement optional single-image intake, Signal ingestion, and multimodal vision analysis
- Add image intake service with format validation (JPEG, PNG, WebP) and EXIF/GPS stripping
- Enforce strict single-image rule across Web and Signal attachment channels
- Implement token-optimized vision downscaling and JPEG compression
- Add IMAGE_CONTEXT pipeline stage with OmniRoute vision routing and resilient failover
- Seed and manage versioned idea-image-interpreter prompt in catalog
- Update Web UI with responsive image picker, preview chip, and Visual Context tab
- Add comprehensive automated test suite in test_image_intake.py
- Update README and Labyricorn devlog
2026-08-23 01:40:22 -07:00

158 lines
5.7 KiB
Python

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