PDF-accessibility-saas/backend/app/deps.py
Vadym Samoilenko fc6f4a12e6 Phase 2+3: FastAPI backend + multi-tenancy schema
Backend (replaces PHP api.php + auth.php):
- FastAPI app with routers: jobs, auth, billing
- Supabase JWT authentication in deps.py
- Celery + Redis job queue (process_pdf_task)
- MinIO S3-compatible storage service
- PDF checker wrapper (delegates to enterprise_pdf_checker.py)
- Stripe billing: checkout, portal, webhook handler

Multi-tenancy (Phase 3):
- Alembic migration 001: workspaces, workspace_members, jobs, usage_events
- Row-Level Security on all tenant tables via app.workspace_id session var
- Monthly quota enforcement per workspace (402 on exceeded)
- Plan tiers: free(5) / pro(100) / business(unlimited)

Config:
- pydantic-settings based config.py (no hardcoded values)
- docker-compose.yml rewritten: postgres, redis, minio, api, celery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:46:05 +01:00

58 lines
2.1 KiB
Python

"""FastAPI dependencies — auth, workspace context."""
from __future__ import annotations
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.db import get_db
settings = get_settings()
bearer = HTTPBearer()
class CurrentUser:
def __init__(self, user_id: str, email: str, workspace_id: str):
self.user_id = user_id
self.email = email
self.workspace_id = workspace_id
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(bearer),
db: AsyncSession = Depends(get_db),
) -> CurrentUser:
token = credentials.credentials
try:
payload = jwt.decode(
token,
settings.supabase_jwt_secret,
algorithms=["HS256"],
audience="authenticated",
)
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
user_id: str = payload.get("sub")
email: str = payload.get("email", "")
if not user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
# Resolve the user's current workspace (first workspace they're a member of)
row = await db.execute(
text("SELECT workspace_id FROM workspace_members WHERE user_id = :uid LIMIT 1"),
{"uid": user_id},
)
result = row.fetchone()
if not result:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No workspace found — complete signup")
workspace_id = str(result[0])
# Set workspace context for RLS — all subsequent queries in this session see only this workspace
await db.execute(text("SET LOCAL app.workspace_id = :wid"), {"wid": workspace_id})
return CurrentUser(user_id=user_id, email=email, workspace_id=workspace_id)