Merge branch 'main' of https://git.kellsupport.com/KellEngineering/complycore
This commit is contained in:
commit
49cd1ba5b8
|
|
@ -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)
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -1,28 +1,33 @@
|
||||||
from sqlalchemy import create_engine
|
# backend/main.py
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Load .env vars
|
from fastapi import FastAPI
|
||||||
load_dotenv()
|
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
|
app = FastAPI()
|
||||||
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
|
@app.get("/healthz")
|
||||||
DATABASE_URL = (
|
def health_check():
|
||||||
f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require"
|
return {"status": "ok"}
|
||||||
)
|
|
||||||
|
|
||||||
# Use NullPool to defer to Supabase's pooler
|
@app.get("/supabase-check")
|
||||||
from sqlalchemy.pool import NullPool
|
def supabase_check():
|
||||||
engine = create_engine(DATABASE_URL, poolclass=NullPool)
|
try:
|
||||||
|
with engine.connect() as conn:
|
||||||
try:
|
result = conn.execute(text("SELECT current_database(), current_user;"))
|
||||||
with engine.connect() as conn:
|
db, user = result.fetchone()
|
||||||
print("✅ Supabase Session Pooler connection successful.")
|
return {"status": "connected", "db": db, "user": user}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Connection failed: {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")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,5 @@ uvicorn[standard]==0.27.1
|
||||||
sqlalchemy==2.0.30
|
sqlalchemy==2.0.30
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
|
python-jose
|
||||||
|
requests
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
@ -10,6 +10,8 @@ services:
|
||||||
- internal_only
|
- internal_only
|
||||||
expose:
|
expose:
|
||||||
- "8000"
|
- "8000"
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue