Skip to content

Commit f9fc137

Browse files
authored
Merge pull request #3 from DevanshuNEU/feat/supabase-auth
Feat/supabase auth
2 parents a000d24 + 3bef5f2 commit f9fc137

30 files changed

Lines changed: 2871 additions & 337 deletions

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ PINECONE_INDEX_NAME=codeintel
1313
# Supabase (Required)
1414
# Get from: https://app.supabase.com/project/_/settings/api
1515
SUPABASE_URL=https://your-project.supabase.co
16-
SUPABASE_KEY=eyJ...
16+
SUPABASE_ANON_KEY=eyJ...
17+
SUPABASE_JWT_SECRET=your-jwt-secret # From Project Settings → API → JWT Secret
1718

1819
# Backend API
1920
API_KEY=change-this-secret-key-for-production

backend/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ PINECONE_INDEX_NAME=codeintel
55

66
# Supabase
77
SUPABASE_URL=https://your-project.supabase.co
8-
SUPABASE_KEY=your_supabase_anon_key_here
8+
SUPABASE_ANON_KEY=your_supabase_anon_key_here
9+
SUPABASE_JWT_SECRET=your_jwt_secret_here
910

1011
# Backend API
1112
BACKEND_API_URL=http://localhost:8000

backend/main.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
CodeIntel Backend API
33
FastAPI backend for codebase intelligence
44
"""
5-
from fastapi import FastAPI, HTTPException, Header, WebSocket, WebSocketDisconnect
5+
from fastapi import FastAPI, HTTPException, Header, WebSocket, WebSocketDisconnect, Depends
66
from fastapi.middleware.cors import CORSMiddleware
77
from pydantic import BaseModel
88
from typing import Optional, List
@@ -25,12 +25,19 @@
2525
from services.supabase_service import get_supabase_service
2626
from services.input_validator import InputValidator, CostController
2727

28+
# Import routers
29+
from routes.auth import router as auth_router
30+
from middleware.auth import get_current_user
31+
2832
app = FastAPI(
2933
title="CodeIntel API",
3034
description="Codebase Intelligence API for MCP",
3135
version="0.2.0"
3236
)
3337

38+
# Include routers
39+
app.include_router(auth_router)
40+
3441
# CORS middleware
3542
app.add_middleware(
3643
CORSMiddleware,
@@ -135,21 +142,23 @@ async def health_check():
135142

136143

137144
@app.get("/api/repos")
138-
async def list_repositories(api_key: str = Header(None, alias="Authorization")):
139-
"""List all repositories"""
140-
verify_api_key(api_key)
145+
async def list_repositories(current_user: dict = Depends(get_current_user)):
146+
"""List all repositories for authenticated user"""
147+
user_id = current_user["user_id"]
141148

149+
# TODO: Filter repos by user_id once we add user_id column to repositories table
150+
# For now, return all repos (will fix in next section)
142151
repos = repo_manager.list_repos()
143152
return {"repositories": repos}
144153

145154

146155
@app.post("/api/repos")
147156
async def add_repository(
148157
request: AddRepoRequest,
149-
api_key: str = Header(None, alias="Authorization")
158+
current_user: dict = Depends(get_current_user)
150159
):
151160
"""Add a new repository with validation and cost controls"""
152-
key_data = verify_api_key(api_key)
161+
user_id = current_user["user_id"]
153162

154163
# Validate repository name
155164
valid_name, name_error = InputValidator.validate_repo_name(request.name)
@@ -162,10 +171,9 @@ async def add_repository(
162171
raise HTTPException(status_code=400, detail=f"Invalid Git URL: {url_error}")
163172

164173
# Check repository limit
165-
user_id = key_data.get("user_id")
166-
api_key_hash = hashlib.sha256(api_key.replace("Bearer ", "").encode()).hexdigest()
174+
user_id_hash = hashlib.sha256(user_id.encode()).hexdigest()
167175

168-
can_add, limit_error = cost_controller.check_repo_limit(user_id, api_key_hash)
176+
can_add, limit_error = cost_controller.check_repo_limit(user_id, user_id_hash)
169177
if not can_add:
170178
raise HTTPException(status_code=429, detail=limit_error)
171179

@@ -175,7 +183,7 @@ async def add_repository(
175183
git_url=request.git_url,
176184
branch=request.branch,
177185
user_id=user_id,
178-
api_key_hash=api_key_hash
186+
api_key_hash=user_id_hash
179187
)
180188

181189
# Check repo size before allowing indexing

backend/middleware/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Authentication middleware package"""
2+
from .auth import get_current_user, get_optional_user
3+
4+
__all__ = ["get_current_user", "get_optional_user"]

backend/middleware/auth.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
Authentication Middleware
3+
Protects routes requiring authentication
4+
"""
5+
from fastapi import Depends, HTTPException, status
6+
from fastapi.security import HTTPBearer
7+
from fastapi.security.http import HTTPAuthorizationCredentials
8+
from typing import Dict, Any, Optional
9+
from services.auth import get_auth_service
10+
11+
# HTTP Bearer token scheme
12+
security = HTTPBearer()
13+
14+
15+
async def get_current_user(
16+
credentials: HTTPAuthorizationCredentials = Depends(security)
17+
) -> Dict[str, Any]:
18+
"""
19+
Dependency to get current authenticated user from JWT token
20+
21+
Usage:
22+
@app.get("/protected")
23+
async def protected_route(user: Dict = Depends(get_current_user)):
24+
return {"user_id": user["user_id"]}
25+
26+
Returns:
27+
Dict with user_id, email, and metadata
28+
29+
Raises:
30+
HTTPException: 401 if token invalid
31+
"""
32+
auth_service = get_auth_service()
33+
return auth_service.verify_jwt(credentials.credentials)
34+
35+
36+
async def get_optional_user(
37+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False))
38+
) -> Optional[Dict[str, Any]]:
39+
"""
40+
Optional authentication - returns None if no token provided
41+
42+
Usage:
43+
@app.get("/optional-auth")
44+
async def route(user: Optional[Dict] = Depends(get_optional_user)):
45+
if user:
46+
return {"message": f"Hello {user['email']}"}
47+
return {"message": "Hello guest"}
48+
"""
49+
if not credentials:
50+
return None
51+
52+
try:
53+
auth_service = get_auth_service()
54+
return auth_service.verify_jwt(credentials.credentials)
55+
except:
56+
return None

backend/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ httpx>=0.27.0
55
python-dotenv>=1.0.0
66
pydantic>=2.0.0
77
pydantic-settings>=2.0.0
8+
email-validator>=2.0.0 # Required for EmailStr validation
89

910
# WebSocket support
1011
websockets>=13.0
@@ -29,6 +30,7 @@ aiofiles>=24.0.0
2930

3031
# Supabase (postgrest-py handled as dependency)
3132
supabase>=2.0.0
33+
pyjwt>=2.8.0 # JWT token verification for Supabase Auth
3234

3335
# Testing
3436
pytest>=8.0.0

backend/routes/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""API Routes package"""
2+
from .auth import router as auth_router
3+
4+
__all__ = ["auth_router"]

backend/routes/auth.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Authentication Routes
3+
Handles user signup, login, and session management
4+
"""
5+
from fastapi import APIRouter, HTTPException, Depends, status
6+
from pydantic import BaseModel, EmailStr
7+
from typing import Optional, Dict, Any
8+
from services.auth import get_auth_service
9+
from middleware.auth import get_current_user
10+
11+
# Create router
12+
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
13+
14+
15+
# Request/Response Models
16+
class SignupRequest(BaseModel):
17+
email: EmailStr
18+
password: str
19+
github_username: Optional[str] = None
20+
21+
22+
class LoginRequest(BaseModel):
23+
email: EmailStr
24+
password: str
25+
26+
27+
class RefreshRequest(BaseModel):
28+
refresh_token: str
29+
30+
31+
class AuthResponse(BaseModel):
32+
user: Dict[str, Any]
33+
session: Dict[str, Any]
34+
35+
36+
# Routes
37+
@router.post("/signup", response_model=AuthResponse)
38+
async def signup(request: SignupRequest):
39+
"""
40+
Sign up a new user with Supabase Auth
41+
42+
- **email**: Valid email address
43+
- **password**: Password (min 6 characters recommended)
44+
- **github_username**: Optional GitHub username for profile
45+
46+
Returns user data and session tokens (access_token, refresh_token)
47+
"""
48+
auth_service = get_auth_service()
49+
return await auth_service.signup(
50+
email=request.email,
51+
password=request.password,
52+
github_username=request.github_username
53+
)
54+
55+
56+
@router.post("/login", response_model=AuthResponse)
57+
async def login(request: LoginRequest):
58+
"""
59+
Login with email and password
60+
61+
- **email**: Registered email address
62+
- **password**: User password
63+
64+
Returns user data and session tokens
65+
"""
66+
auth_service = get_auth_service()
67+
return await auth_service.login(
68+
email=request.email,
69+
password=request.password
70+
)
71+
72+
73+
@router.post("/refresh")
74+
async def refresh(request: RefreshRequest):
75+
"""
76+
Refresh access token using refresh token
77+
78+
- **refresh_token**: Valid refresh token from login/signup
79+
80+
Returns new access token
81+
"""
82+
auth_service = get_auth_service()
83+
return await auth_service.refresh_session(request.refresh_token)
84+
85+
86+
@router.post("/logout")
87+
async def logout(user: Dict = Depends(get_current_user)):
88+
"""
89+
Logout current user and invalidate session
90+
91+
Requires: Valid JWT token in Authorization header
92+
"""
93+
auth_service = get_auth_service()
94+
return await auth_service.logout(token="") # Supabase handles session
95+
96+
97+
@router.get("/me")
98+
async def get_current_user_info(user: Dict = Depends(get_current_user)):
99+
"""
100+
Get current authenticated user information
101+
102+
Requires: Valid JWT token in Authorization header
103+
104+
Returns user profile data
105+
"""
106+
return {"user": user}

0 commit comments

Comments
 (0)