Skip to content

Commit 040b967

Browse files
authored
Merge pull request #4 from tanzilahmed0/task-b5-auth-endpoints
Completed Task B5 - Enhanced Authentication System
2 parents 514d781 + 15c9428 commit 040b967

19 files changed

Lines changed: 2922 additions & 770 deletions

backend/api/auth.py

Lines changed: 203 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,229 @@
1-
import os
1+
import logging
22
import uuid
3-
from datetime import datetime, timedelta
4-
from typing import Any, Dict
3+
from typing import Optional
54

65
import jwt
76
from fastapi import APIRouter, Depends, HTTPException
8-
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
7+
from fastapi.security import HTTPBearer
98

10-
from models.response_schemas import (
11-
ApiResponse,
12-
AuthResponse,
13-
LoginRequest,
14-
RefreshTokenRequest,
15-
User,
16-
)
9+
from models.response_schemas import ApiResponse, AuthResponse, LoginRequest, User
10+
from models.user import UserInDB
11+
from services.auth_service import AuthService
1712

18-
router = APIRouter(prefix="/auth", tags=["authentication"])
13+
# Configure logging
14+
logger = logging.getLogger(__name__)
15+
16+
router = APIRouter(prefix="/auth", tags=["Authentication"])
17+
auth_service = AuthService()
1918
security = HTTPBearer()
2019

21-
# Mock user database
22-
MOCK_USERS = {
23-
"google_user_123": {
24-
"id": "user_001",
25-
"email": "john.doe@example.com",
26-
"name": "John Doe",
27-
"avatar_url": "https://lh3.googleusercontent.com/a/default-user",
28-
"created_at": "2025-01-01T00:00:00Z",
29-
"last_sign_in_at": "2025-01-01T12:00:00Z",
30-
}
31-
}
32-
33-
# Mock JWT settings
34-
JWT_SECRET = os.getenv("JWT_SECRET", "mock_secret_key_for_development")
35-
ALGORITHM = "HS256"
36-
ACCESS_TOKEN_EXPIRE_MINUTES = 60
37-
38-
39-
def create_access_token(data: Dict[str, Any]) -> str:
40-
"""Create JWT access token"""
41-
to_encode = data.copy()
42-
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
43-
to_encode.update({"exp": expire})
44-
return jwt.encode(to_encode, JWT_SECRET, algorithm=ALGORITHM)
45-
46-
47-
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
48-
"""Verify JWT token and return user_id"""
49-
try:
50-
payload = jwt.decode(
51-
credentials.credentials, JWT_SECRET, algorithms=[ALGORITHM]
52-
)
53-
user_id: str = payload.get("sub")
54-
if user_id is None:
55-
raise HTTPException(status_code=401, detail="Invalid token")
56-
return user_id
57-
except jwt.PyJWTError:
58-
raise HTTPException(status_code=401, detail="Invalid token")
20+
21+
def get_current_user_token(token: str = Depends(security)) -> str:
22+
"""Extract token from Authorization header"""
23+
return token.credentials
5924

6025

6126
@router.post("/google")
6227
async def login_with_google(request: LoginRequest) -> ApiResponse[AuthResponse]:
63-
"""Mock Google OAuth login"""
64-
# Mock Google token validation
65-
if not request.google_token.startswith("mock_google_token"):
66-
raise HTTPException(status_code=401, detail="Invalid Google token")
28+
"""Google OAuth login with enhanced error handling"""
29+
try:
30+
logger.info("Received Google OAuth login request")
6731

68-
# Mock user from Google token
69-
user_data = MOCK_USERS["google_user_123"]
70-
user = User(**user_data)
32+
# Validate request
33+
if not request.google_token or not request.google_token.strip():
34+
logger.warning("Empty Google token received")
35+
raise HTTPException(status_code=400, detail="Google token is required")
7136

72-
# Create JWT tokens
73-
access_token = create_access_token(data={"sub": user.id})
74-
refresh_token = str(uuid.uuid4())
37+
user, access_token, refresh_token, is_new_user = auth_service.login_with_google(
38+
request.google_token.strip()
39+
)
7540

76-
auth_response = AuthResponse(
77-
user=user,
78-
access_token=access_token,
79-
refresh_token=refresh_token,
80-
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
81-
)
41+
# Convert UserInDB to the response model directly
42+
user_response = User(
43+
id=str(user.id),
44+
email=user.email,
45+
name=user.name,
46+
avatar_url=user.avatar_url,
47+
created_at=user.created_at.isoformat(),
48+
last_sign_in_at=user.updated_at.isoformat(), # Using updated_at for last sign-in
49+
)
8250

83-
return ApiResponse(success=True, data=auth_response)
51+
auth_response = AuthResponse(
52+
user=user_response,
53+
access_token=access_token,
54+
refresh_token=refresh_token,
55+
expires_in=auth_service.access_token_expire_minutes * 60,
56+
)
57+
58+
logger.info(
59+
f"Google OAuth login successful for user: {user.email}, is_new_user: {is_new_user}"
60+
)
61+
return ApiResponse(
62+
success=True,
63+
data=auth_response,
64+
message=(
65+
"Login successful"
66+
if not is_new_user
67+
else "Account created and login successful"
68+
),
69+
)
70+
71+
except HTTPException:
72+
# Re-raise HTTPException without modification
73+
raise
74+
except ValueError as e:
75+
logger.error(f"Google OAuth validation error: {str(e)}")
76+
raise HTTPException(status_code=401, detail=f"Invalid Google token: {str(e)}")
77+
except Exception as e:
78+
logger.error(f"Google OAuth login failed: {str(e)}")
79+
raise HTTPException(status_code=500, detail=f"Authentication failed: {str(e)}")
8480

8581

8682
@router.get("/me")
87-
async def get_current_user(user_id: str = Depends(verify_token)) -> ApiResponse[User]:
88-
"""Get current user information"""
89-
# Mock user lookup
90-
for mock_user in MOCK_USERS.values():
91-
if mock_user["id"] == user_id:
92-
user = User(**mock_user)
93-
return ApiResponse(success=True, data=user)
83+
async def get_current_user(
84+
token: str = Depends(get_current_user_token),
85+
) -> ApiResponse[User]:
86+
"""Get current user information with enhanced error handling"""
87+
try:
88+
logger.info("Received current user request")
89+
90+
user = auth_service.get_current_user(token)
91+
92+
# Convert UserInDB to the response model directly
93+
user_response = User(
94+
id=str(user.id),
95+
email=user.email,
96+
name=user.name,
97+
avatar_url=user.avatar_url,
98+
created_at=user.created_at.isoformat(),
99+
last_sign_in_at=user.updated_at.isoformat(), # Using updated_at for last sign-in
100+
)
101+
102+
logger.info(f"Current user request successful for: {user.email}")
103+
return ApiResponse(success=True, data=user_response)
94104

95-
raise HTTPException(status_code=404, detail="User not found")
105+
except jwt.InvalidTokenError as e:
106+
logger.warning(f"Invalid token in current user request: {str(e)}")
107+
raise HTTPException(
108+
status_code=401, detail=f"Invalid or expired token: {str(e)}"
109+
)
110+
except Exception as e:
111+
logger.error(f"Current user request failed: {str(e)}")
112+
raise HTTPException(
113+
status_code=500, detail=f"Failed to get user information: {str(e)}"
114+
)
96115

97116

98117
@router.post("/logout")
99-
async def logout(user_id: str = Depends(verify_token)) -> ApiResponse[Dict[str, str]]:
100-
"""Logout current user"""
101-
return ApiResponse(success=True, data={"message": "Logged out successfully"})
118+
async def logout(token: str = Depends(get_current_user_token)) -> ApiResponse[dict]:
119+
"""Logout current user with enhanced logging"""
120+
try:
121+
logger.info("Received logout request")
122+
123+
# Verify token and get user for logging
124+
user = auth_service.get_current_user(token)
125+
126+
# Revoke tokens (placeholder implementation)
127+
success = auth_service.revoke_user_tokens(str(user.id))
128+
129+
if success:
130+
logger.info(f"Logout successful for user: {user.email}")
131+
return ApiResponse(
132+
success=True,
133+
data={"message": "Logged out successfully"},
134+
message="You have been logged out",
135+
)
136+
else:
137+
logger.error(f"Token revocation failed for user: {user.email}")
138+
raise HTTPException(status_code=500, detail="Logout failed")
139+
140+
except jwt.InvalidTokenError as e:
141+
logger.warning(f"Invalid token in logout request: {str(e)}")
142+
raise HTTPException(
143+
status_code=401, detail=f"Invalid or expired token: {str(e)}"
144+
)
145+
except Exception as e:
146+
logger.error(f"Logout failed: {str(e)}")
147+
raise HTTPException(status_code=500, detail=f"Logout failed: {str(e)}")
102148

103149

104150
@router.post("/refresh")
105-
async def refresh_token(request: RefreshTokenRequest) -> ApiResponse[Dict[str, Any]]:
106-
"""Refresh access token"""
107-
# Mock refresh token validation
108-
if not request.refresh_token:
109-
raise HTTPException(status_code=401, detail="Invalid refresh token")
110-
111-
# Create new access token
112-
new_access_token = create_access_token(data={"sub": "user_001"})
113-
114-
return ApiResponse(
115-
success=True,
116-
data={
117-
"access_token": new_access_token,
118-
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
119-
},
120-
)
151+
async def refresh_token(request: dict) -> ApiResponse[AuthResponse]:
152+
"""Refresh access token with enhanced validation"""
153+
try:
154+
logger.info("Received token refresh request")
155+
156+
# Validate request
157+
refresh_token = request.get("refresh_token")
158+
if not refresh_token or not refresh_token.strip():
159+
logger.warning("Empty refresh token received")
160+
raise HTTPException(status_code=400, detail="Refresh token is required")
161+
162+
new_access_token, user = auth_service.refresh_access_token(
163+
refresh_token.strip()
164+
)
165+
166+
# Convert to response format
167+
user_response = User(
168+
id=str(user.id),
169+
email=user.email,
170+
name=user.name,
171+
avatar_url=user.avatar_url,
172+
created_at=user.created_at.isoformat(),
173+
last_sign_in_at=user.updated_at.isoformat(),
174+
)
175+
176+
auth_response = AuthResponse(
177+
user=user_response,
178+
access_token=new_access_token,
179+
refresh_token=refresh_token, # Keep the same refresh token
180+
expires_in=auth_service.access_token_expire_minutes * 60,
181+
)
182+
183+
logger.info(f"Token refresh successful for user: {user.email}")
184+
return ApiResponse(
185+
success=True, data=auth_response, message="Token refreshed successfully"
186+
)
187+
188+
except HTTPException:
189+
# Re-raise HTTPException without modification
190+
raise
191+
except jwt.InvalidTokenError as e:
192+
logger.warning(f"Invalid refresh token: {str(e)}")
193+
raise HTTPException(
194+
status_code=401, detail=f"Invalid or expired refresh token: {str(e)}"
195+
)
196+
except Exception as e:
197+
logger.error(f"Token refresh failed: {str(e)}")
198+
raise HTTPException(status_code=500, detail=f"Token refresh failed: {str(e)}")
199+
200+
201+
@router.get("/health")
202+
async def auth_health_check() -> ApiResponse[dict]:
203+
"""Enhanced authentication service health check"""
204+
try:
205+
logger.info("Received auth health check request")
206+
207+
health_data = auth_service.health_check()
208+
209+
# Determine HTTP status based on health
210+
if health_data.get("status") == "healthy":
211+
logger.info("Auth health check passed")
212+
return ApiResponse(
213+
success=True,
214+
data=health_data,
215+
message="Authentication service is healthy",
216+
)
217+
else:
218+
logger.warning(f"Auth health check failed: {health_data}")
219+
raise HTTPException(
220+
status_code=503,
221+
detail=f"Authentication service is unhealthy: {health_data.get('error', 'Unknown error')}",
222+
)
223+
224+
except HTTPException:
225+
# Re-raise HTTPException without modification
226+
raise
227+
except Exception as e:
228+
logger.error(f"Auth health check error: {str(e)}")
229+
raise HTTPException(status_code=500, detail=f"Health check failed: {str(e)}")

backend/api/chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
from fastapi import APIRouter, Depends, HTTPException, Query
77

8-
from api.auth import verify_token
98
from api.projects import MOCK_PROJECTS
9+
from middleware.auth_middleware import verify_token
1010
from models.response_schemas import (
1111
ApiResponse,
1212
ChatMessage,

backend/api/health.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from fastapi import APIRouter
66

7-
from services.database_service import db_service
7+
from services.database_service import get_db_service
88
from services.redis_service import redis_service
99
from services.storage_service import storage_service
1010

@@ -44,7 +44,7 @@ async def health_check() -> Dict[str, Any]:
4444
}
4545

4646
# Check all services in production
47-
database_health = db_service.health_check()
47+
database_health = get_db_service().health_check()
4848
redis_health = redis_service.health_check()
4949
storage_health = storage_service.health_check()
5050

backend/api/projects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from fastapi import APIRouter, Depends, HTTPException, Query
66

7-
from api.auth import verify_token
7+
from middleware.auth_middleware import verify_token
88
from models.response_schemas import (
99
ApiResponse,
1010
ColumnMetadata,

0 commit comments

Comments
 (0)