From add3904a73d9a0f30b165ce3833706133337660c Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Tue, 22 Jul 2025 18:56:40 -0400 Subject: [PATCH] Supabase initialization and connection --- backend/auth/__init__.py | 0 backend/auth/jwt.py | 40 ++++++++++++++++++++++++++++++ backend/db/__init__.py | 0 backend/db/session.py | 30 +++++++++++++++++++++++ backend/main.py | 53 ++++++++++++++++++++++------------------ backend/requirements.txt | 2 ++ backend/test-db.py | 28 +++++++++++++++++++++ docker-compose.yml | 2 ++ 8 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 backend/auth/__init__.py create mode 100644 backend/auth/jwt.py create mode 100644 backend/db/__init__.py create mode 100644 backend/db/session.py create mode 100644 backend/test-db.py diff --git a/backend/auth/__init__.py b/backend/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/auth/jwt.py b/backend/auth/jwt.py new file mode 100644 index 0000000..fa4fd9e --- /dev/null +++ b/backend/auth/jwt.py @@ -0,0 +1,40 @@ +# backend/auth/jwt.py + +import requests +from jose import jwt +from jose.exceptions import JWTError +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from typing import Dict + +SUPABASE_PROJECT_ID = "lcoretjgpauozmuoedus" # <-- Replace with your project ref +SUPABASE_JWKS_URL = f"https://{SUPABASE_PROJECT_ID}.supabase.co/auth/v1/keys" + +# Fetch JWKs from Supabase +jwks = requests.get(SUPABASE_JWKS_URL).json() + +# Define FastAPI's bearer auth scheme +auth_scheme = HTTPBearer() + +# Decode + verify JWT token +def verify_jwt_token(token: str) -> Dict: + try: + header = jwt.get_unverified_header(token) + kid = header["kid"] + + key = next((k for k in jwks["keys"] if k["kid"] == kid), None) + if key is None: + raise HTTPException(status_code=403, detail="Invalid Supabase JWT: No matching key") + + payload = jwt.decode(token, key, algorithms=["RS256"], options={"verify_aud": False}) + return payload + + except JWTError as e: + raise HTTPException(status_code=403, detail=f"Invalid Supabase JWT: {str(e)}") + +# Dependency for protected endpoints +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(auth_scheme) +) -> Dict: + token = credentials.credentials + return verify_jwt_token(token) diff --git a/backend/db/__init__.py b/backend/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/db/session.py b/backend/db/session.py new file mode 100644 index 0000000..f70d36f --- /dev/null +++ b/backend/db/session.py @@ -0,0 +1,30 @@ +# backend/db/session.py + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool +from dotenv import load_dotenv +import os + +# Load environment variables +load_dotenv() + +# Read .env settings +USER = os.getenv("user") +PASSWORD = os.getenv("password") +HOST = os.getenv("host") +PORT = os.getenv("port") +DBNAME = os.getenv("dbname") + +# Supabase Transaction Pooler (IPv4-safe) URI +DATABASE_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require" + +# SQLAlchemy engine with NullPool for Supabase +engine = create_engine(DATABASE_URL, poolclass=NullPool) + +# Session factory (used for queries) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base class for ORM models +Base = declarative_base() diff --git a/backend/main.py b/backend/main.py index 39c92dd..b0573dd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,28 +1,33 @@ -from sqlalchemy import create_engine -from dotenv import load_dotenv -import os +# backend/main.py -# Load .env vars -load_dotenv() +from fastapi import FastAPI +from fastapi.responses import JSONResponse +from sqlalchemy import text +from db.session import engine +from fastapi import Depends +from auth.jwt import get_current_user -# Get env vars -USER = os.getenv("user") -PASSWORD = os.getenv("password") -HOST = os.getenv("host") -PORT = os.getenv("port") -DBNAME = os.getenv("dbname") +app = FastAPI() -# Full SQLAlchemy URI for Session Pooler -DATABASE_URL = ( - f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require" -) +@app.get("/healthz") +def health_check(): + return {"status": "ok"} -# Use NullPool to defer to Supabase's pooler -from sqlalchemy.pool import NullPool -engine = create_engine(DATABASE_URL, poolclass=NullPool) - -try: - with engine.connect() as conn: - print("✅ Supabase Session Pooler connection successful.") -except Exception as e: - print(f"❌ Connection failed: {e}") +@app.get("/supabase-check") +def supabase_check(): + try: + with engine.connect() as conn: + result = conn.execute(text("SELECT current_database(), current_user;")) + db, user = result.fetchone() + return {"status": "connected", "db": db, "user": user} + except Exception as e: + return JSONResponse(status_code=500, content={"status": "error", "error": str(e)}) + +@app.get("/me") +def me(user: dict = Depends(get_current_user)): + return { + "id": user.get("sub"), + "email": user.get("email"), + "role": user.get("role"), + "tenant_id": user.get("tenant_id", "unknown") + } diff --git a/backend/requirements.txt b/backend/requirements.txt index a917863..e89da41 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,3 +3,5 @@ uvicorn[standard]==0.27.1 sqlalchemy==2.0.30 psycopg2-binary==2.9.9 python-dotenv==1.0.1 +python-jose +requests diff --git a/backend/test-db.py b/backend/test-db.py new file mode 100644 index 0000000..39c92dd --- /dev/null +++ b/backend/test-db.py @@ -0,0 +1,28 @@ +from sqlalchemy import create_engine +from dotenv import load_dotenv +import os + +# Load .env vars +load_dotenv() + +# Get env vars +USER = os.getenv("user") +PASSWORD = os.getenv("password") +HOST = os.getenv("host") +PORT = os.getenv("port") +DBNAME = os.getenv("dbname") + +# Full SQLAlchemy URI for Session Pooler +DATABASE_URL = ( + f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require" +) + +# Use NullPool to defer to Supabase's pooler +from sqlalchemy.pool import NullPool +engine = create_engine(DATABASE_URL, poolclass=NullPool) + +try: + with engine.connect() as conn: + print("✅ Supabase Session Pooler connection successful.") +except Exception as e: + print(f"❌ Connection failed: {e}") diff --git a/docker-compose.yml b/docker-compose.yml index a39f052..b984bf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - internal_only expose: - "8000" + ports: + - "8000:8000" networks: