|
1 | | -import os |
| 1 | +import logging |
2 | 2 | import uuid |
3 | | -from datetime import datetime, timedelta |
4 | | -from typing import Any, Dict |
| 3 | +from typing import Optional |
5 | 4 |
|
6 | 5 | import jwt |
7 | 6 | from fastapi import APIRouter, Depends, HTTPException |
8 | | -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer |
| 7 | +from fastapi.security import HTTPBearer |
9 | 8 |
|
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 |
17 | 12 |
|
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() |
19 | 18 | security = HTTPBearer() |
20 | 19 |
|
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 |
59 | 24 |
|
60 | 25 |
|
61 | 26 | @router.post("/google") |
62 | 27 | 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") |
67 | 31 |
|
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") |
71 | 36 |
|
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 | + ) |
75 | 40 |
|
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 | + ) |
82 | 50 |
|
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)}") |
84 | 80 |
|
85 | 81 |
|
86 | 82 | @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) |
94 | 104 |
|
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 | + ) |
96 | 115 |
|
97 | 116 |
|
98 | 117 | @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)}") |
102 | 148 |
|
103 | 149 |
|
104 | 150 | @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)}") |
0 commit comments