xml-pipeline/bloxserver/api/dependencies.py
dullfig a5c00c1e90 Add BloxServer API scaffold + architecture docs
BloxServer API (FastAPI + SQLAlchemy async):
- Database models: users, flows, triggers, executions, usage tracking
- Clerk JWT auth with dev mode bypass for local testing
- SQLite support for local dev, PostgreSQL for production
- CRUD routes for flows, triggers, executions
- Public webhook endpoint with token auth
- Health/readiness endpoints
- Pydantic schemas with camelCase aliases for frontend
- Docker + docker-compose setup

Architecture documentation:
- Librarian architecture with RLM-powered query engine
- Stripe billing integration (usage-based, trials, webhooks)
- LLM abstraction layer (rate limiting, semantic cache, failover)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 22:04:25 -08:00

236 lines
7 KiB
Python

"""
FastAPI dependencies for authentication and database access.
Uses Clerk for JWT validation.
"""
from __future__ import annotations
import os
from typing import Annotated
from uuid import UUID
import httpx
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from bloxserver.api.models.database import get_db
from bloxserver.api.models.tables import UserRecord
# Dev mode - skip auth for local testing
DEV_MODE = os.getenv("ENV", "development") == "development" and not os.getenv("CLERK_ISSUER")
# Clerk configuration
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "")
CLERK_JWKS_URL = f"{CLERK_ISSUER}/.well-known/jwks.json" if CLERK_ISSUER else ""
# Security scheme
security = HTTPBearer(auto_error=False)
# =============================================================================
# JWT Validation (Clerk)
# =============================================================================
async def get_clerk_jwks() -> dict:
"""Fetch Clerk's JWKS for JWT validation."""
async with httpx.AsyncClient() as client:
response = await client.get(CLERK_JWKS_URL)
response.raise_for_status()
return response.json()
async def validate_clerk_token(token: str) -> dict:
"""
Validate a Clerk JWT token and return the payload.
In production, use a proper JWT library with caching.
This is a simplified version for the scaffold.
"""
import jwt
from jwt import PyJWKClient
try:
# Get signing key from Clerk's JWKS
jwks_client = PyJWKClient(CLERK_JWKS_URL)
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Decode and validate
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=os.getenv("CLERK_AUDIENCE"),
issuer=CLERK_ISSUER,
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
)
except jwt.InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid token: {e}",
)
# =============================================================================
# Current User Dependency
# =============================================================================
class CurrentUser:
"""Authenticated user context."""
def __init__(self, user: UserRecord, clerk_payload: dict):
self.user = user
self.clerk_payload = clerk_payload
@property
def id(self) -> UUID:
return self.user.id
@property
def clerk_id(self) -> str:
return self.user.clerk_id
@property
def email(self) -> str:
return self.user.email
@property
def tier(self) -> str:
return self.user.tier.value
async def get_current_user(
request: Request,
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> CurrentUser:
"""
Dependency that validates the JWT and returns the current user.
Creates the user record if this is their first request (synced from Clerk).
In DEV_MODE without Clerk configured, returns a test user.
"""
# Dev mode - create/return a test user without auth
if DEV_MODE:
dev_clerk_id = "dev_user_001"
result = await db.execute(
select(UserRecord).where(UserRecord.clerk_id == dev_clerk_id)
)
user = result.scalar_one_or_none()
if not user:
from bloxserver.api.models.tables import Tier
user = UserRecord(
clerk_id=dev_clerk_id,
email="dev@localhost",
name="Dev User",
tier=Tier.PRO, # Give dev user Pro access
)
db.add(user)
await db.flush()
return CurrentUser(user=user, clerk_payload={"sub": dev_clerk_id, "dev": True})
# Production mode - require Clerk auth
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing authentication token",
headers={"WWW-Authenticate": "Bearer"},
)
# Validate JWT
payload = await validate_clerk_token(credentials.credentials)
clerk_id = payload.get("sub")
if not clerk_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token: missing subject",
)
# Look up or create user
result = await db.execute(
select(UserRecord).where(UserRecord.clerk_id == clerk_id)
)
user = result.scalar_one_or_none()
if not user:
# First login - create user record from Clerk data
user = UserRecord(
clerk_id=clerk_id,
email=payload.get("email", f"{clerk_id}@unknown"),
name=payload.get("name"),
avatar_url=payload.get("image_url"),
)
db.add(user)
await db.flush() # Get the ID without committing
return CurrentUser(user=user, clerk_payload=payload)
# Type alias for cleaner route signatures
AuthenticatedUser = Annotated[CurrentUser, Depends(get_current_user)]
DbSession = Annotated[AsyncSession, Depends(get_db)]
# =============================================================================
# Optional Auth (for public endpoints)
# =============================================================================
async def get_optional_user(
request: Request,
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> CurrentUser | None:
"""
Like get_current_user, but returns None instead of raising if not authenticated.
"""
if not credentials:
return None
try:
return await get_current_user(request, credentials, db)
except HTTPException:
return None
OptionalUser = Annotated[CurrentUser | None, Depends(get_optional_user)]
# =============================================================================
# Tier Checks
# =============================================================================
def require_tier(*allowed_tiers: str):
"""
Dependency factory that requires the user to be on one of the allowed tiers.
Usage:
@router.post("/wasm", dependencies=[Depends(require_tier("pro", "enterprise"))])
"""
async def check_tier(user: AuthenticatedUser) -> None:
if user.tier not in allowed_tiers:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"This feature requires one of: {', '.join(allowed_tiers)}",
)
return check_tier
RequirePro = Depends(require_tier("pro", "enterprise", "high_frequency"))
RequireEnterprise = Depends(require_tier("enterprise", "high_frequency"))