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