File size: 3,946 Bytes
674fb4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Request, UploadFile, File, Form, Query
from typing import List, Dict, Any, Optional
import re
from ...core.neo4j_store import Neo4jStore
from ...retrieval.agent import AgentRetrievalSystem
from ...ingestion.pipeline import IngestionPipeline
from ...config import settings
from ...api.models import RegisterRequest, LoginRequest, TokenResponse
from ...api.auth import get_current_user, User, get_password_hash, verify_password, create_access_token
from fastapi import status
from datetime import timedelta
import redis
from ..dependencies import get_graph_store, get_retrieval_agent, get_ingestion_pipeline, get_redis_client
router = APIRouter()
from ...core.storage import get_storage
storage = get_storage()
_TENANT_ID_RE = re.compile(r'^[A-Za-z0-9_\-]{1,64}$')
@router.post("/api/auth/register", response_model=User, tags=["Authentication"])
async def register(payload: RegisterRequest, request: Request):
"""Register a new user"""
existing_user = await request.app.state.graph_store.get_user(payload.username)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered"
)
hashed_password = get_password_hash(payload.password)
# SECURITY: Prevent unauthorized admin registration
safe_scopes = [s for s in payload.scopes if s != "admin"]
if not safe_scopes:
safe_scopes = ["read", "write"]
# SECURITY: Sanitize tenant_id — only alphanumeric, underscore, hyphen (max 64 chars)
raw_tenant_id = payload.tenant_id if hasattr(payload, "tenant_id") and payload.tenant_id else settings.default_tenant_id
if not _TENANT_ID_RE.match(raw_tenant_id):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="tenant_id must contain only letters, digits, underscores, or hyphens (max 64 chars)"
)
safe_tenant_id = raw_tenant_id
user_data = {
"username": payload.username,
"hashed_password": hashed_password,
"email": payload.email,
"full_name": payload.full_name,
"disabled": False,
"scopes": safe_scopes,
"tenant_id": safe_tenant_id
}
await request.app.state.graph_store.create_user(user_data)
return User(
username=payload.username,
email=payload.email,
full_name=payload.full_name,
disabled=False,
scopes=safe_scopes,
tenant_id=user_data["tenant_id"]
)
@router.post("/api/auth/login", response_model=TokenResponse, tags=["Authentication"])
async def login(payload: LoginRequest, request: Request):
"""
Login and get access token
Verifies user against Neo4j database
"""
user_data = await request.app.state.graph_store.get_user(payload.username)
if not user_data or not verify_password(payload.password, user_data["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if user_data.get("disabled"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
# Create access token
access_token = create_access_token(
data={
"sub": user_data["username"],
"scopes": user_data.get("scopes", ["read", "write"])
},
expires_delta=timedelta(minutes=settings.access_token_expire_minutes)
)
return TokenResponse(access_token=access_token)
@router.get("/api/auth/me", response_model=User, tags=["Authentication"])
async def get_me(request: Request, current_user: User = Depends(get_current_user)):
"""Get current user information"""
return current_user
# Document Upload & Ingestion Endpoints
|