Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions api/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from api.routes.auth import auth_router, init_auth
from api.routes.graphs import graphs_router
from api.routes.database import database_router
from api.routes.tokens import tokens_router

# Load environment variables from .env file
load_dotenv()
Expand Down Expand Up @@ -82,6 +83,7 @@ def create_app():
app.include_router(auth_router)
app.include_router(graphs_router, prefix="/graphs")
app.include_router(database_router)
app.include_router(tokens_router, prefix="/api/tokens")

@app.exception_handler(Exception)
async def handle_oauth_error(request: Request, exc: Exception):
Expand Down
82 changes: 78 additions & 4 deletions api/auth/user_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
from functools import wraps
from typing import Tuple, Optional, Dict, Any

import requests
from fastapi import Request, HTTPException, status
from fastapi.responses import JSONResponse
from authlib.integrations.starlette_client import OAuth

from api.extensions import db
Expand Down Expand Up @@ -277,14 +275,88 @@ async def validate_and_cache_user(request: Request) -> Tuple[Optional[Dict[str,
request.session.pop("user_info", None)
return None, False

async def validate_api_token_user(token: str) -> Tuple[Optional[Dict[str, Any]], bool]:
"""
Validate an API token and return user info if valid.
Returns (user_info, is_authenticated).
"""
try:
if not token:
return None, False

# Import here to avoid circular imports
from api.routes.tokens import validate_api_token

user_email = validate_api_token(token)
if not user_email:
return None, False

# Get user info from Organizations graph
organizations_graph = db.select_graph("Organizations")

query = """
MATCH (user:User {email: $email})<-[:AUTHENTICATES]-(identity:Identity)
RETURN user, identity
LIMIT 1
"""

result = organizations_graph.query(query, {"email": user_email})

if result.result_set:
user = result.result_set[0][0]
identity = result.result_set[0][1]

# Create user_info similar to OAuth format
first_name = user.properties.get('first_name', '')
last_name = user.properties.get('last_name', '')
full_name = f"{first_name} {last_name}".strip()

user_info = {
"id": identity.properties.get("provider_user_id", user_email),
"email": user_email,
"name": full_name,
"picture": identity.properties.get("picture", ""),
"provider": "api_token",
}

return user_info, True

return None, False

except Exception as e:
logging.error("Error validating API token user: %s", e)
return None, False


def token_required(func):
"""Decorator to protect FastAPI routes with token authentication.
Automatically refreshes tokens if expired.
Supports both OAuth and API token authentication.
"""

@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
try:
# First, try API token authentication
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
api_token = auth_header[7:] # Remove "Bearer " prefix
user_info, is_authenticated = await validate_api_token_user(api_token)

if is_authenticated:
# Attach user_id to request.state
request.state.user_id = user_info.get("id")
if not request.state.user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized - Invalid token user"
)

# Also store user_info for compatibility
request.state.user_info = user_info
return await func(request, *args, **kwargs)

# Fall back to OAuth session authentication
user_info, is_authenticated = await validate_and_cache_user(request)

if not is_authenticated:
Expand All @@ -295,7 +367,7 @@ async def wrapper(request: Request, *args, **kwargs):
if not is_authenticated:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized - Please log in"
detail="Unauthorized - Please log in or provide a valid API token"
)

# Attach user_id to request.state (like FASTAPI's g.user_id)
Expand All @@ -307,6 +379,8 @@ async def wrapper(request: Request, *args, **kwargs):
detail="Unauthorized - Invalid user"
)

# Also store user_info for compatibility
request.state.user_info = user_info
return await func(request, *args, **kwargs)

except HTTPException:
Expand All @@ -317,6 +391,6 @@ async def wrapper(request: Request, *args, **kwargs):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized - Authentication error"
)
) from e

return wrapper
2 changes: 1 addition & 1 deletion api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
if __name__ == "__main__":
import os
import uvicorn

# Read FASTAPI_DEBUG to determine debug mode
debug_mode = os.environ.get('FASTAPI_DEBUG', 'False').lower() == 'true'
uvicorn.run(
Expand Down
Loading