Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ QDRANT_COLLECTION_NAME=your_collection_name
# Langfuse Configuration
LANGFUSE_SECRET_KEY=your_langfuse_secret_key
LANGFUSE_PUBLIC_KEY=your_langfuse_public_key
LANGFUSE_HOST=your_langfuse_host_url
LANGFUSE_HOST=your_langfuse_host_url

# Auth0 Configuration
AUTH0_DOMAIN=your_auth0_domain
AUTH0_AUDIENCE=your_auth0_audience

# Supabase Configuration
SUPABASE_URL=your_supabase_url
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
24 changes: 24 additions & 0 deletions backend/app/controllers/users_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from fastapi import HTTPException

from app.services.users_service import sync_auth0_user


async def sync_current_user_controller(
payload: dict,
) -> dict:
"""
Sync authenticated Auth0 user into Supabase.
"""

auth0_sub = payload.get("sub")

if not auth0_sub:
raise HTTPException(
status_code=400,
detail="Missing Auth0 subject claim",
)

return {
"status": "success",
"user": sync_auth0_user(payload),
}
83 changes: 83 additions & 0 deletions backend/app/core/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from typing import Any

import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import PyJWKClient
from jwt.exceptions import PyJWKClientError

from app.core.config import settings

bearer_scheme = HTTPBearer(auto_error=False)
_jwks_client: PyJWKClient | None = None


def _get_auth0_config() -> tuple[str, str, PyJWKClient]:
if not settings.AUTH0_DOMAIN or not settings.AUTH0_AUDIENCE:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Auth0 configuration is missing",
)

issuer = f"https://{settings.AUTH0_DOMAIN}/"
jwks_url = f"{issuer}.well-known/jwks.json"

global _jwks_client
if _jwks_client is None:
# PyJWKClient downloads and caches Auth0 signing keys lazily,
# so missing network access does not block app startup.
_jwks_client = PyJWKClient(jwks_url)

return issuer, settings.AUTH0_AUDIENCE, _jwks_client


async def get_bearer_token(
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> str:
"""
Extract raw bearer token from Authorization header.
"""
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing bearer token",
headers={"WWW-Authenticate": "Bearer"},
)

return credentials.credentials


async def verify_auth0_token(
access_token: str = Depends(get_bearer_token),
) -> dict[str, Any]:
"""
Verify Auth0 JWT access token and return decoded payload.
"""

try:
issuer, audience, jwks_client = _get_auth0_config()
signing_key = jwks_client.get_signing_key_from_jwt(
access_token
).key

payload = jwt.decode(
access_token,
signing_key,
algorithms=["RS256"],
audience=audience,
issuer=issuer,
)

return payload

except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
)

except (jwt.InvalidTokenError, PyJWKClientError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token",
)
18 changes: 17 additions & 1 deletion backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from pydantic_settings import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional


class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)

# Frontend Config
FRONTEND_URL: str

Expand All @@ -24,4 +31,13 @@ class Settings(BaseSettings):
LANGFUSE_PUBLIC_KEY: str
LANGFUSE_HOST: str

# Auth0 Config
AUTH0_DOMAIN: Optional[str] = None
AUTH0_AUDIENCE: Optional[str] = None

# Supabase Config
SUPABASE_URL: Optional[str] = None
SUPABASE_SERVICE_ROLE_KEY: Optional[str] = None


settings = Settings()
22 changes: 21 additions & 1 deletion backend/app/core/singleton.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from langfuse.openai import AzureOpenAI, OpenAI
from qdrant_client import QdrantClient
from app.core.config import settings
from supabase import Client, create_client

openai_client = None
qdrant_client = None
supabase_client = None


# OpenAI
Expand All @@ -22,7 +24,7 @@ def connect_openai():
api_key=settings.AZURE_API_KEY,
base_url=settings.AZURE_ENDPOINT,
project=None,
organization=None
organization=None,
)
return openai_client

Expand All @@ -44,3 +46,21 @@ def get_qdrant_client():
connect_qdrant()
qdrant = qdrant_client
return qdrant


def connect_supabase():
global supabase_client
if supabase_client is None:
if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_ROLE_KEY:
raise ValueError("Supabase configuration is missing")
supabase_client = create_client(
settings.SUPABASE_URL,
settings.SUPABASE_SERVICE_ROLE_KEY,
)
return supabase_client


def get_supabase_client() -> Client:
connect_supabase()
supabase = supabase_client
return supabase
2 changes: 2 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from app.routes.llms_router import router as llms_router
from app.routes.qdrant_router import router as qdrant_router
from app.routes.knowledge_base_router import router as knowledge_base_router
from app.routes.users_router import router as users_router

ascii_art = """
╔════════════════════════════════════════════════════════════════════════════════════╗
Expand Down Expand Up @@ -56,6 +57,7 @@ async def startup_event():
app.include_router(llms_router, tags=["LLM with Tool Calling"])
app.include_router(vcelldb_router, tags=["VCellDB API Wrapper"])
app.include_router(qdrant_router, tags=["Qdrant Vector DB"], prefix="/qdrant")
app.include_router(users_router, tags=["Users"])

if __name__ == "__main__":
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
25 changes: 20 additions & 5 deletions backend/app/routes/llms_router.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends

from app.controllers.llms_controller import (
get_llm_response,
analyse_biomodel_controller,
analyse_vcml_controller,
analyse_diagram_controller,
)
from app.core.auth import verify_auth0_token

router = APIRouter()


@router.post("/query")
async def query_llm(conversation_history: dict):
async def query_llm(
conversation_history: dict,
_payload: dict = Depends(verify_auth0_token),
):
"""
Endpoint to query the LLM and execute the necessary tools.
Args:
Expand All @@ -25,7 +30,11 @@ async def query_llm(conversation_history: dict):


@router.post("/analyse/{biomodel_id}")
async def analyse_biomodel(biomodel_id: str, user_prompt: str):
async def analyse_biomodel(
biomodel_id: str,
user_prompt: str,
_payload: dict = Depends(verify_auth0_token),
):
"""
Endpoint to analyze a biomodel using the LLM service.
Args:
Expand All @@ -39,7 +48,10 @@ async def analyse_biomodel(biomodel_id: str, user_prompt: str):


@router.post("/analyse/{biomodel_id}/vcml")
async def analyse_vcml(biomodel_id: str):
async def analyse_vcml(
biomodel_id: str,
_payload: dict = Depends(verify_auth0_token),
):
"""
Endpoint to analyze VCML content for a given biomodel.
Args:
Expand All @@ -52,7 +64,10 @@ async def analyse_vcml(biomodel_id: str):


@router.post("/analyse/{biomodel_id}/diagram")
async def analyse_diagram(biomodel_id: str):
async def analyse_diagram(
biomodel_id: str,
_payload: dict = Depends(verify_auth0_token),
):
"""
Endpoint to analyze diagram for a given biomodel.
Args:
Expand Down
19 changes: 19 additions & 0 deletions backend/app/routes/users_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import APIRouter, Depends

from app.controllers.users_controller import (
sync_current_user_controller,
)
from app.core.auth import verify_auth0_token

router = APIRouter()


@router.post("/users/me", response_model=dict)
async def sync_current_user(
payload: dict = Depends(verify_auth0_token),
):
"""
endpoint to Sync authenticated Auth0 user into Supabase.
"""

return await sync_current_user_controller(payload)
34 changes: 34 additions & 0 deletions backend/app/services/users_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from datetime import datetime, timezone

from app.core.singleton import get_supabase_client


def sync_auth0_user(payload: dict) -> dict | None:
"""
Create or update the local Supabase user row from verified Auth0 claims.
"""
auth0_sub = payload["sub"]
email = payload.get("email")

user_values = {
"auth0_sub": auth0_sub,
"last_login": datetime.now(timezone.utc).isoformat(),
}
if email is not None:
user_values["email"] = email
if payload.get("name") is not None:
user_values["name"] = payload.get("name")

supabase = get_supabase_client()

# AuthSync can run more than once during client hydration; upsert keeps this idempotent.
response = (
supabase.table("users")
.upsert(
user_values,
on_conflict="auth0_sub",
)
.execute()
)

return response.data[0] if response.data else None
Loading
Loading