Spaces:
Running
Running
Commit ·
2d38c65
1
Parent(s): d74c068
Enhance error handling and logging across authentication and user management endpoints
Browse files- Improved error handling in authentication dependencies to raise detailed HTTP exceptions for various failure scenarios.
- Added logging for missing credentials, token verification failures, and database errors in authentication processes.
- Enhanced user creation endpoints with validation checks for required fields and improved error responses.
- Implemented detailed logging for user management actions, including creation, updates, and deactivation.
- Added request logging middleware to track incoming requests and their processing times.
- Standardized error responses for validation errors and database issues throughout the application.
- Improved health check and database status endpoints with better error handling and logging.
- ERROR_HANDLING_GUIDE.md +514 -0
- ERROR_HANDLING_IMPLEMENTATION_SUMMARY.md +416 -0
- app/auth/controllers/router.py +174 -52
- app/dependencies/auth.py +134 -18
- app/internal/router.py +124 -8
- app/main.py +242 -13
- app/system_users/controllers/router.py +343 -44
ERROR_HANDLING_GUIDE.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Error Handling Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This authentication microservice implements comprehensive error handling across all routes and components. The error handling system includes:
|
| 6 |
+
|
| 7 |
+
- **Global exception handlers** for consistent error responses
|
| 8 |
+
- **Request/response logging middleware** for debugging and monitoring
|
| 9 |
+
- **Detailed validation error messages** for client feedback
|
| 10 |
+
- **Proper HTTP status codes** following REST standards
|
| 11 |
+
- **Security-aware error messages** to prevent information leakage
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Global Exception Handlers
|
| 16 |
+
|
| 17 |
+
Located in `app/main.py`, the following global exception handlers are implemented:
|
| 18 |
+
|
| 19 |
+
### 1. Request Validation Errors (422)
|
| 20 |
+
|
| 21 |
+
Handles Pydantic validation errors when request data doesn't match expected schema.
|
| 22 |
+
|
| 23 |
+
**Response Format:**
|
| 24 |
+
```json
|
| 25 |
+
{
|
| 26 |
+
"success": false,
|
| 27 |
+
"error": "Validation Error",
|
| 28 |
+
"detail": "The request contains invalid data",
|
| 29 |
+
"errors": [
|
| 30 |
+
{
|
| 31 |
+
"field": "email",
|
| 32 |
+
"message": "value is not a valid email address",
|
| 33 |
+
"type": "value_error.email"
|
| 34 |
+
}
|
| 35 |
+
]
|
| 36 |
+
}
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### 2. JWT Token Errors (401)
|
| 40 |
+
|
| 41 |
+
Handles invalid or expired JWT tokens.
|
| 42 |
+
|
| 43 |
+
**Response Format:**
|
| 44 |
+
```json
|
| 45 |
+
{
|
| 46 |
+
"success": false,
|
| 47 |
+
"error": "Authentication Error",
|
| 48 |
+
"detail": "Invalid or expired token",
|
| 49 |
+
"headers": {"WWW-Authenticate": "Bearer"}
|
| 50 |
+
}
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### 3. MongoDB Errors (500/503)
|
| 54 |
+
|
| 55 |
+
Handles database connection and operation failures.
|
| 56 |
+
|
| 57 |
+
**Response Format:**
|
| 58 |
+
```json
|
| 59 |
+
{
|
| 60 |
+
"success": false,
|
| 61 |
+
"error": "Database Connection Error",
|
| 62 |
+
"detail": "Unable to connect to the database. Please try again later."
|
| 63 |
+
}
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### 4. General Exceptions (500)
|
| 67 |
+
|
| 68 |
+
Catches all unhandled exceptions with detailed logging.
|
| 69 |
+
|
| 70 |
+
**Response Format:**
|
| 71 |
+
```json
|
| 72 |
+
{
|
| 73 |
+
"success": false,
|
| 74 |
+
"error": "Internal Server Error",
|
| 75 |
+
"detail": "An unexpected error occurred. Please try again later.",
|
| 76 |
+
"request_id": "140234567890"
|
| 77 |
+
}
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## HTTP Status Codes
|
| 83 |
+
|
| 84 |
+
### Success Codes
|
| 85 |
+
- **200 OK**: Successful GET, PUT, DELETE operations
|
| 86 |
+
- **201 Created**: Successful POST operations (user creation)
|
| 87 |
+
|
| 88 |
+
### Client Error Codes
|
| 89 |
+
- **400 Bad Request**: Invalid input data, missing required fields
|
| 90 |
+
- **401 Unauthorized**: Missing, invalid, or expired authentication token
|
| 91 |
+
- **403 Forbidden**: Insufficient permissions for the requested operation
|
| 92 |
+
- **404 Not Found**: Requested resource doesn't exist
|
| 93 |
+
- **422 Unprocessable Entity**: Request validation failed
|
| 94 |
+
|
| 95 |
+
### Server Error Codes
|
| 96 |
+
- **500 Internal Server Error**: Unexpected server-side error
|
| 97 |
+
- **503 Service Unavailable**: Database connection unavailable
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## Route-Specific Error Handling
|
| 102 |
+
|
| 103 |
+
### Authentication Routes (`/auth`)
|
| 104 |
+
|
| 105 |
+
#### POST `/auth/login`
|
| 106 |
+
**Possible Errors:**
|
| 107 |
+
- `400`: Missing email/phone or password
|
| 108 |
+
- `401`: Invalid credentials, account locked, or inactive account
|
| 109 |
+
- `500`: Database error, token generation error
|
| 110 |
+
|
| 111 |
+
**Example:**
|
| 112 |
+
```json
|
| 113 |
+
{
|
| 114 |
+
"detail": "Account is locked until 2025-12-28T15:30:00"
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
#### POST `/auth/refresh`
|
| 119 |
+
**Possible Errors:**
|
| 120 |
+
- `400`: Missing refresh token
|
| 121 |
+
- `401`: Invalid or expired refresh token, user not found or inactive
|
| 122 |
+
- `500`: Database error, token generation error
|
| 123 |
+
|
| 124 |
+
#### GET `/auth/me`
|
| 125 |
+
**Possible Errors:**
|
| 126 |
+
- `401`: Invalid or missing authentication token
|
| 127 |
+
- `500`: Error retrieving user information
|
| 128 |
+
|
| 129 |
+
#### GET `/auth/access-roles`
|
| 130 |
+
**Possible Errors:**
|
| 131 |
+
- `500`: Database error fetching roles
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
### User Management Routes (`/auth/users`)
|
| 136 |
+
|
| 137 |
+
#### POST `/auth/users`
|
| 138 |
+
**Possible Errors:**
|
| 139 |
+
- `400`: Missing required fields, password too short, invalid data
|
| 140 |
+
- `403`: Insufficient permissions
|
| 141 |
+
- `500`: Database error, user creation failed
|
| 142 |
+
|
| 143 |
+
**Validation Rules:**
|
| 144 |
+
- Username: Required, non-empty
|
| 145 |
+
- Email: Required, valid email format
|
| 146 |
+
- Password: Minimum 8 characters
|
| 147 |
+
|
| 148 |
+
#### GET `/auth/users`
|
| 149 |
+
**Possible Errors:**
|
| 150 |
+
- `400`: Invalid pagination parameters (page < 1, page_size < 1)
|
| 151 |
+
- `403`: Insufficient permissions
|
| 152 |
+
- `500`: Database error
|
| 153 |
+
|
| 154 |
+
#### GET `/auth/users/{user_id}`
|
| 155 |
+
**Possible Errors:**
|
| 156 |
+
- `400`: Invalid or missing user ID
|
| 157 |
+
- `403`: Insufficient permissions
|
| 158 |
+
- `404`: User not found
|
| 159 |
+
- `500`: Database error
|
| 160 |
+
|
| 161 |
+
#### PUT `/auth/users/{user_id}`
|
| 162 |
+
**Possible Errors:**
|
| 163 |
+
- `400`: Invalid data, no data provided, invalid user ID
|
| 164 |
+
- `403`: Insufficient permissions
|
| 165 |
+
- `404`: User not found
|
| 166 |
+
- `500`: Database error
|
| 167 |
+
|
| 168 |
+
#### PUT `/auth/change-password`
|
| 169 |
+
**Possible Errors:**
|
| 170 |
+
- `400`: Missing passwords, password too short, same as current password
|
| 171 |
+
- `401`: Current password incorrect
|
| 172 |
+
- `500`: Database error
|
| 173 |
+
|
| 174 |
+
**Validation Rules:**
|
| 175 |
+
- Current password: Required
|
| 176 |
+
- New password: Minimum 8 characters, different from current
|
| 177 |
+
|
| 178 |
+
#### DELETE `/auth/users/{user_id}`
|
| 179 |
+
**Possible Errors:**
|
| 180 |
+
- `400`: Cannot deactivate own account, invalid user ID
|
| 181 |
+
- `403`: Insufficient permissions
|
| 182 |
+
- `404`: User not found
|
| 183 |
+
- `500`: Database error
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
### Internal API Routes (`/internal/system-users`)
|
| 188 |
+
|
| 189 |
+
#### POST `/internal/system-users/from-employee`
|
| 190 |
+
**Possible Errors:**
|
| 191 |
+
- `400`: Missing employee_id, email, first_name, merchant_id, or role_id
|
| 192 |
+
- `500`: Database error, user creation failed
|
| 193 |
+
|
| 194 |
+
#### POST `/internal/system-users/from-merchant`
|
| 195 |
+
**Possible Errors:**
|
| 196 |
+
- `400`: Missing merchant_id, email, merchant_name, merchant_type, or role_id
|
| 197 |
+
- `400`: Invalid email format
|
| 198 |
+
- `500`: Database error, user creation failed
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## Request Logging Middleware
|
| 203 |
+
|
| 204 |
+
All requests are logged with the following information:
|
| 205 |
+
|
| 206 |
+
### Request Start Log
|
| 207 |
+
```
|
| 208 |
+
INFO: Request started: POST /auth/login
|
| 209 |
+
Extra: {
|
| 210 |
+
"request_id": "140234567890",
|
| 211 |
+
"method": "POST",
|
| 212 |
+
"path": "/auth/login",
|
| 213 |
+
"client": "192.168.1.100",
|
| 214 |
+
"user_agent": "Mozilla/5.0..."
|
| 215 |
+
}
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
### Request Complete Log
|
| 219 |
+
```
|
| 220 |
+
INFO: Request completed: POST /auth/login - Status: 200
|
| 221 |
+
Extra: {
|
| 222 |
+
"request_id": "140234567890",
|
| 223 |
+
"method": "POST",
|
| 224 |
+
"path": "/auth/login",
|
| 225 |
+
"status_code": 200,
|
| 226 |
+
"process_time": "0.234s"
|
| 227 |
+
}
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
### Custom Response Headers
|
| 231 |
+
- `X-Process-Time`: Request processing time in seconds
|
| 232 |
+
- `X-Request-ID`: Unique request identifier for tracking
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## Authentication Dependencies
|
| 237 |
+
|
| 238 |
+
Located in `app/dependencies/auth.py`:
|
| 239 |
+
|
| 240 |
+
### `get_current_user()`
|
| 241 |
+
**Errors:**
|
| 242 |
+
- `401`: Invalid token, missing token, token verification failed
|
| 243 |
+
- `403`: User account not active
|
| 244 |
+
- `500`: Database error
|
| 245 |
+
|
| 246 |
+
### `require_admin_role()`
|
| 247 |
+
**Errors:**
|
| 248 |
+
- `401`: Authentication errors (from `get_current_user`)
|
| 249 |
+
- `403`: User doesn't have admin privileges
|
| 250 |
+
|
| 251 |
+
**Authorized Roles:**
|
| 252 |
+
- `super_admin`
|
| 253 |
+
- `admin`
|
| 254 |
+
- `role_super_admin`
|
| 255 |
+
- `role_company_admin`
|
| 256 |
+
|
| 257 |
+
### `require_super_admin_role()`
|
| 258 |
+
**Errors:**
|
| 259 |
+
- `401`: Authentication errors
|
| 260 |
+
- `403`: User doesn't have super admin privileges
|
| 261 |
+
|
| 262 |
+
**Authorized Roles:**
|
| 263 |
+
- `super_admin`
|
| 264 |
+
- `role_super_admin`
|
| 265 |
+
|
| 266 |
+
### `require_permission(permission)`
|
| 267 |
+
**Errors:**
|
| 268 |
+
- `401`: Authentication errors
|
| 269 |
+
- `403`: User doesn't have required permission
|
| 270 |
+
|
| 271 |
+
**Note:** Admins and super admins have all permissions by default.
|
| 272 |
+
|
| 273 |
+
---
|
| 274 |
+
|
| 275 |
+
## Error Logging
|
| 276 |
+
|
| 277 |
+
### Log Levels
|
| 278 |
+
|
| 279 |
+
#### INFO
|
| 280 |
+
- Successful operations (login, logout, user creation)
|
| 281 |
+
- Request start/complete
|
| 282 |
+
|
| 283 |
+
#### WARNING
|
| 284 |
+
- Failed login attempts
|
| 285 |
+
- Permission denied attempts
|
| 286 |
+
- Missing data or invalid requests
|
| 287 |
+
|
| 288 |
+
#### ERROR
|
| 289 |
+
- Database errors
|
| 290 |
+
- Unexpected exceptions
|
| 291 |
+
- Token generation failures
|
| 292 |
+
- Service unavailability
|
| 293 |
+
|
| 294 |
+
### Log Format
|
| 295 |
+
|
| 296 |
+
All errors include:
|
| 297 |
+
- Timestamp
|
| 298 |
+
- Log level
|
| 299 |
+
- Message
|
| 300 |
+
- Exception traceback (for ERROR level)
|
| 301 |
+
- Context data (user_id, request_id, etc.)
|
| 302 |
+
|
| 303 |
+
**Example:**
|
| 304 |
+
```python
|
| 305 |
+
logger.error(
|
| 306 |
+
f"Failed to create user: {str(e)}",
|
| 307 |
+
exc_info=True,
|
| 308 |
+
extra={
|
| 309 |
+
"user_id": user_id,
|
| 310 |
+
"operation": "create_user"
|
| 311 |
+
}
|
| 312 |
+
)
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
---
|
| 316 |
+
|
| 317 |
+
## Best Practices
|
| 318 |
+
|
| 319 |
+
### 1. **Always Re-raise HTTPException**
|
| 320 |
+
```python
|
| 321 |
+
try:
|
| 322 |
+
# operation
|
| 323 |
+
except HTTPException:
|
| 324 |
+
raise # Don't wrap HTTPException
|
| 325 |
+
except Exception as e:
|
| 326 |
+
# Handle other exceptions
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
### 2. **Validate Input Early**
|
| 330 |
+
```python
|
| 331 |
+
if not user_id or not user_id.strip():
|
| 332 |
+
raise HTTPException(
|
| 333 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 334 |
+
detail="User ID is required"
|
| 335 |
+
)
|
| 336 |
+
```
|
| 337 |
+
|
| 338 |
+
### 3. **Log Sensitive Operations**
|
| 339 |
+
```python
|
| 340 |
+
logger.info(f"User {username} performed action", extra={...})
|
| 341 |
+
logger.warning(f"Failed attempt: {reason}")
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
### 4. **Use Specific Error Messages**
|
| 345 |
+
```python
|
| 346 |
+
# Good
|
| 347 |
+
detail="Password must be at least 8 characters long"
|
| 348 |
+
|
| 349 |
+
# Avoid
|
| 350 |
+
detail="Invalid input"
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
+
### 5. **Don't Leak Sensitive Information**
|
| 354 |
+
```python
|
| 355 |
+
# Good
|
| 356 |
+
detail="Invalid credentials"
|
| 357 |
+
|
| 358 |
+
# Avoid
|
| 359 |
+
detail="User not found: john@example.com"
|
| 360 |
+
```
|
| 361 |
+
|
| 362 |
+
---
|
| 363 |
+
|
| 364 |
+
## Testing Error Scenarios
|
| 365 |
+
|
| 366 |
+
### Testing Authentication Errors
|
| 367 |
+
|
| 368 |
+
```bash
|
| 369 |
+
# Missing token
|
| 370 |
+
curl -X GET http://localhost:8002/auth/me
|
| 371 |
+
|
| 372 |
+
# Invalid token
|
| 373 |
+
curl -X GET http://localhost:8002/auth/me \
|
| 374 |
+
-H "Authorization: Bearer invalid_token"
|
| 375 |
+
|
| 376 |
+
# Expired token
|
| 377 |
+
curl -X GET http://localhost:8002/auth/me \
|
| 378 |
+
-H "Authorization: Bearer <expired_token>"
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
### Testing Validation Errors
|
| 382 |
+
|
| 383 |
+
```bash
|
| 384 |
+
# Missing required field
|
| 385 |
+
curl -X POST http://localhost:8002/auth/login \
|
| 386 |
+
-H "Content-Type: application/json" \
|
| 387 |
+
-d '{"email_or_phone": "test@example.com"}'
|
| 388 |
+
|
| 389 |
+
# Invalid email format
|
| 390 |
+
curl -X POST http://localhost:8002/auth/users \
|
| 391 |
+
-H "Authorization: Bearer <token>" \
|
| 392 |
+
-H "Content-Type: application/json" \
|
| 393 |
+
-d '{"username": "test", "email": "invalid-email"}'
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
### Testing Permission Errors
|
| 397 |
+
|
| 398 |
+
```bash
|
| 399 |
+
# Non-admin trying to list users
|
| 400 |
+
curl -X GET http://localhost:8002/auth/users \
|
| 401 |
+
-H "Authorization: Bearer <user_token>"
|
| 402 |
+
```
|
| 403 |
+
|
| 404 |
+
---
|
| 405 |
+
|
| 406 |
+
## Monitoring and Debugging
|
| 407 |
+
|
| 408 |
+
### Using Request IDs
|
| 409 |
+
|
| 410 |
+
Every request gets a unique `request_id` that appears in:
|
| 411 |
+
- Response headers (`X-Request-ID`)
|
| 412 |
+
- Log entries
|
| 413 |
+
- Error responses (500 errors)
|
| 414 |
+
|
| 415 |
+
**Track a request:**
|
| 416 |
+
```bash
|
| 417 |
+
# Get request ID from response
|
| 418 |
+
curl -i http://localhost:8002/health
|
| 419 |
+
|
| 420 |
+
# Search logs
|
| 421 |
+
grep "request_id.*140234567890" app.log
|
| 422 |
+
```
|
| 423 |
+
|
| 424 |
+
### Performance Monitoring
|
| 425 |
+
|
| 426 |
+
Check `X-Process-Time` header to monitor endpoint performance:
|
| 427 |
+
```bash
|
| 428 |
+
curl -i http://localhost:8002/auth/me \
|
| 429 |
+
-H "Authorization: Bearer <token>"
|
| 430 |
+
|
| 431 |
+
# X-Process-Time: 0.234
|
| 432 |
+
```
|
| 433 |
+
|
| 434 |
+
---
|
| 435 |
+
|
| 436 |
+
## Error Response Examples
|
| 437 |
+
|
| 438 |
+
### 400 Bad Request
|
| 439 |
+
```json
|
| 440 |
+
{
|
| 441 |
+
"success": false,
|
| 442 |
+
"error": "Bad Request",
|
| 443 |
+
"detail": "Email is required"
|
| 444 |
+
}
|
| 445 |
+
```
|
| 446 |
+
|
| 447 |
+
### 401 Unauthorized
|
| 448 |
+
```json
|
| 449 |
+
{
|
| 450 |
+
"success": false,
|
| 451 |
+
"error": "Authentication Error",
|
| 452 |
+
"detail": "Could not validate credentials"
|
| 453 |
+
}
|
| 454 |
+
```
|
| 455 |
+
|
| 456 |
+
### 403 Forbidden
|
| 457 |
+
```json
|
| 458 |
+
{
|
| 459 |
+
"success": false,
|
| 460 |
+
"error": "Forbidden",
|
| 461 |
+
"detail": "Admin privileges required"
|
| 462 |
+
}
|
| 463 |
+
```
|
| 464 |
+
|
| 465 |
+
### 404 Not Found
|
| 466 |
+
```json
|
| 467 |
+
{
|
| 468 |
+
"success": false,
|
| 469 |
+
"error": "Not Found",
|
| 470 |
+
"detail": "User not found"
|
| 471 |
+
}
|
| 472 |
+
```
|
| 473 |
+
|
| 474 |
+
### 422 Validation Error
|
| 475 |
+
```json
|
| 476 |
+
{
|
| 477 |
+
"success": false,
|
| 478 |
+
"error": "Validation Error",
|
| 479 |
+
"detail": "The request contains invalid data",
|
| 480 |
+
"errors": [
|
| 481 |
+
{
|
| 482 |
+
"field": "email",
|
| 483 |
+
"message": "value is not a valid email address",
|
| 484 |
+
"type": "value_error.email"
|
| 485 |
+
}
|
| 486 |
+
]
|
| 487 |
+
}
|
| 488 |
+
```
|
| 489 |
+
|
| 490 |
+
### 500 Internal Server Error
|
| 491 |
+
```json
|
| 492 |
+
{
|
| 493 |
+
"success": false,
|
| 494 |
+
"error": "Internal Server Error",
|
| 495 |
+
"detail": "An unexpected error occurred. Please try again later.",
|
| 496 |
+
"request_id": "140234567890"
|
| 497 |
+
}
|
| 498 |
+
```
|
| 499 |
+
|
| 500 |
+
---
|
| 501 |
+
|
| 502 |
+
## Summary
|
| 503 |
+
|
| 504 |
+
The error handling implementation provides:
|
| 505 |
+
|
| 506 |
+
✅ **Consistent error responses** across all endpoints
|
| 507 |
+
✅ **Detailed validation feedback** for developers
|
| 508 |
+
✅ **Security-aware messages** that don't leak sensitive data
|
| 509 |
+
✅ **Comprehensive logging** for debugging and monitoring
|
| 510 |
+
✅ **Request tracking** via unique request IDs
|
| 511 |
+
✅ **Performance metrics** via process time headers
|
| 512 |
+
✅ **Proper HTTP status codes** following REST standards
|
| 513 |
+
|
| 514 |
+
For additional support or questions, refer to the main README.md or contact the development team.
|
ERROR_HANDLING_IMPLEMENTATION_SUMMARY.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Error Handling Implementation Summary
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
This document summarizes all the error handling enhancements made to the authentication microservice to ensure robust, production-ready error handling across all routes.
|
| 5 |
+
|
| 6 |
+
## Changes Made
|
| 7 |
+
|
| 8 |
+
### 1. Global Exception Handlers (`app/main.py`)
|
| 9 |
+
|
| 10 |
+
Added comprehensive global exception handlers:
|
| 11 |
+
|
| 12 |
+
#### Added Imports
|
| 13 |
+
```python
|
| 14 |
+
import time
|
| 15 |
+
from fastapi.responses import JSONResponse
|
| 16 |
+
from fastapi.exceptions import RequestValidationError
|
| 17 |
+
from pydantic import ValidationError, BaseModel
|
| 18 |
+
from typing import Optional, List, Dict, Any
|
| 19 |
+
from jose import JWTError
|
| 20 |
+
from pymongo.errors import PyMongoError, ConnectionFailure, OperationFailure
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
#### New Exception Handlers
|
| 24 |
+
1. **RequestValidationError Handler** (422)
|
| 25 |
+
- Handles Pydantic validation errors
|
| 26 |
+
- Returns detailed field-level error information
|
| 27 |
+
- Logs validation failures
|
| 28 |
+
|
| 29 |
+
2. **ValidationError Handler** (422)
|
| 30 |
+
- Handles general Pydantic validation errors
|
| 31 |
+
- Returns detailed error messages
|
| 32 |
+
|
| 33 |
+
3. **JWTError Handler** (401)
|
| 34 |
+
- Handles JWT token errors
|
| 35 |
+
- Returns authentication error response
|
| 36 |
+
- Includes WWW-Authenticate header
|
| 37 |
+
|
| 38 |
+
4. **PyMongoError Handler** (500/503)
|
| 39 |
+
- Handles MongoDB connection failures (503)
|
| 40 |
+
- Handles MongoDB operation failures (500)
|
| 41 |
+
- Provides user-friendly error messages
|
| 42 |
+
|
| 43 |
+
5. **General Exception Handler** (500)
|
| 44 |
+
- Catches all unhandled exceptions
|
| 45 |
+
- Logs with full traceback
|
| 46 |
+
- Includes request ID for tracking
|
| 47 |
+
|
| 48 |
+
#### Request Logging Middleware
|
| 49 |
+
Added middleware to log all requests and responses:
|
| 50 |
+
- Logs request start with method, path, client IP, user agent
|
| 51 |
+
- Logs request completion with status code and processing time
|
| 52 |
+
- Adds custom headers: `X-Process-Time`, `X-Request-ID`
|
| 53 |
+
- Handles exceptions during request processing
|
| 54 |
+
|
| 55 |
+
#### Error Response Models
|
| 56 |
+
```python
|
| 57 |
+
class ErrorDetail(BaseModel):
|
| 58 |
+
field: Optional[str] = None
|
| 59 |
+
message: str
|
| 60 |
+
type: Optional[str] = None
|
| 61 |
+
|
| 62 |
+
class ErrorResponse(BaseModel):
|
| 63 |
+
success: bool = False
|
| 64 |
+
error: str
|
| 65 |
+
detail: str
|
| 66 |
+
errors: Optional[List[ErrorDetail]] = None
|
| 67 |
+
request_id: Optional[str] = None
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
### 2. Authentication Router (`app/auth/controllers/router.py`)
|
| 73 |
+
|
| 74 |
+
Enhanced error handling for all authentication endpoints:
|
| 75 |
+
|
| 76 |
+
#### POST `/auth/login`
|
| 77 |
+
- Added input validation (email/phone and password presence)
|
| 78 |
+
- Wrapped permission fetching in try-catch
|
| 79 |
+
- Wrapped token creation in try-catch
|
| 80 |
+
- Enhanced error logging with context
|
| 81 |
+
- Better error messages for different failure scenarios
|
| 82 |
+
|
| 83 |
+
#### POST `/auth/refresh`
|
| 84 |
+
- Added input validation for refresh token
|
| 85 |
+
- Enhanced token verification error handling
|
| 86 |
+
- Added database error handling
|
| 87 |
+
- Improved user status checking
|
| 88 |
+
- Better logging for failed attempts
|
| 89 |
+
|
| 90 |
+
#### GET `/auth/me`
|
| 91 |
+
- Added AttributeError handling
|
| 92 |
+
- Enhanced error logging
|
| 93 |
+
- Better error messages
|
| 94 |
+
|
| 95 |
+
#### POST `/auth/logout`
|
| 96 |
+
- Added error handling for user access
|
| 97 |
+
- Enhanced logging
|
| 98 |
+
|
| 99 |
+
#### GET `/auth/access-roles`
|
| 100 |
+
- Added null check for roles
|
| 101 |
+
- Enhanced error handling
|
| 102 |
+
- Returns HTTPException instead of dict on error
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
### 3. System Users Router (`app/system_users/controllers/router.py`)
|
| 107 |
+
|
| 108 |
+
Enhanced error handling for all user management endpoints:
|
| 109 |
+
|
| 110 |
+
#### POST `/auth/login`
|
| 111 |
+
- Added comprehensive input validation
|
| 112 |
+
- Wrapped authentication in try-catch
|
| 113 |
+
- Wrapped token creation in try-catch
|
| 114 |
+
- Wrapped user info conversion in try-catch
|
| 115 |
+
- Enhanced error logging
|
| 116 |
+
|
| 117 |
+
#### GET `/auth/me`
|
| 118 |
+
- Added AttributeError handling
|
| 119 |
+
- Enhanced error messages
|
| 120 |
+
|
| 121 |
+
#### POST `/auth/users`
|
| 122 |
+
- Added input validation (username, email, password)
|
| 123 |
+
- Added password length validation (min 8 chars)
|
| 124 |
+
- Enhanced error logging
|
| 125 |
+
- Added ValueError handling
|
| 126 |
+
|
| 127 |
+
#### GET `/auth/users`
|
| 128 |
+
- Added pagination parameter validation
|
| 129 |
+
- Added page and page_size bounds checking
|
| 130 |
+
- Enhanced error logging
|
| 131 |
+
- Added ValueError handling
|
| 132 |
+
|
| 133 |
+
#### POST `/auth/users/list`
|
| 134 |
+
- Added limit validation
|
| 135 |
+
- Added skip validation
|
| 136 |
+
- Enhanced error logging
|
| 137 |
+
- Better validation error handling
|
| 138 |
+
|
| 139 |
+
#### GET `/auth/users/{user_id}`
|
| 140 |
+
- Added user_id validation
|
| 141 |
+
- Enhanced error logging
|
| 142 |
+
- Better 404 handling
|
| 143 |
+
|
| 144 |
+
#### PUT `/auth/users/{user_id}`
|
| 145 |
+
- Added user_id validation
|
| 146 |
+
- Added check for update data presence
|
| 147 |
+
- Enhanced error logging
|
| 148 |
+
- Added ValueError handling
|
| 149 |
+
|
| 150 |
+
#### PUT `/auth/change-password`
|
| 151 |
+
- Added comprehensive password validation
|
| 152 |
+
- Added password length check
|
| 153 |
+
- Added same-password check
|
| 154 |
+
- Enhanced error logging
|
| 155 |
+
- Better failure logging
|
| 156 |
+
|
| 157 |
+
#### DELETE `/auth/users/{user_id}`
|
| 158 |
+
- Added user_id validation
|
| 159 |
+
- Added self-deactivation check with logging
|
| 160 |
+
- Enhanced error logging
|
| 161 |
+
|
| 162 |
+
#### POST `/auth/setup/super-admin`
|
| 163 |
+
- Added comprehensive input validation
|
| 164 |
+
- Added database error handling for user check
|
| 165 |
+
- Added ValueError handling
|
| 166 |
+
- Enhanced error logging
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
|
| 170 |
+
### 4. Internal Router (`app/internal/router.py`)
|
| 171 |
+
|
| 172 |
+
Enhanced error handling for internal API endpoints:
|
| 173 |
+
|
| 174 |
+
#### POST `/internal/system-users/from-employee`
|
| 175 |
+
- Added validation for all required fields:
|
| 176 |
+
- employee_id
|
| 177 |
+
- email
|
| 178 |
+
- first_name
|
| 179 |
+
- merchant_id
|
| 180 |
+
- role_id
|
| 181 |
+
- Wrapped user creation in try-catch
|
| 182 |
+
- Enhanced error logging with context
|
| 183 |
+
- Added ValueError handling
|
| 184 |
+
|
| 185 |
+
#### POST `/internal/system-users/from-merchant`
|
| 186 |
+
- Added validation for all required fields:
|
| 187 |
+
- merchant_id
|
| 188 |
+
- email
|
| 189 |
+
- merchant_name
|
| 190 |
+
- merchant_type
|
| 191 |
+
- role_id
|
| 192 |
+
- Added email format validation
|
| 193 |
+
- Wrapped user creation in try-catch
|
| 194 |
+
- Enhanced error logging with context
|
| 195 |
+
- Added ValueError handling
|
| 196 |
+
|
| 197 |
+
---
|
| 198 |
+
|
| 199 |
+
### 5. Authentication Dependencies (`app/dependencies/auth.py`)
|
| 200 |
+
|
| 201 |
+
Enhanced error handling for authentication dependencies:
|
| 202 |
+
|
| 203 |
+
#### Added Logging
|
| 204 |
+
```python
|
| 205 |
+
import logging
|
| 206 |
+
logger = logging.getLogger(__name__)
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
#### `get_system_user_service()`
|
| 210 |
+
- Added database null check
|
| 211 |
+
- Enhanced error handling
|
| 212 |
+
- Returns 503 on database unavailability
|
| 213 |
+
|
| 214 |
+
#### `get_current_user()`
|
| 215 |
+
- Added credentials validation
|
| 216 |
+
- Wrapped token verification in try-catch
|
| 217 |
+
- Added database error handling
|
| 218 |
+
- Enhanced logging for all failure scenarios
|
| 219 |
+
- Better error messages
|
| 220 |
+
|
| 221 |
+
#### `require_admin_role()`
|
| 222 |
+
- Added logging for unauthorized attempts
|
| 223 |
+
- Enhanced role checking
|
| 224 |
+
- Supports more role types (role_super_admin, role_company_admin)
|
| 225 |
+
|
| 226 |
+
#### `require_super_admin_role()`
|
| 227 |
+
- Added logging for unauthorized attempts
|
| 228 |
+
- Enhanced role checking
|
| 229 |
+
- Supports more role types (role_super_admin)
|
| 230 |
+
|
| 231 |
+
#### `require_permission()`
|
| 232 |
+
- Enhanced permission checking
|
| 233 |
+
- Better admin role handling
|
| 234 |
+
- Added logging for permission denied attempts
|
| 235 |
+
|
| 236 |
+
#### `get_optional_user()`
|
| 237 |
+
- Enhanced error handling
|
| 238 |
+
- Better null checking
|
| 239 |
+
- Debug-level logging
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
## Benefits
|
| 244 |
+
|
| 245 |
+
### 1. **Consistency**
|
| 246 |
+
- All endpoints return errors in the same format
|
| 247 |
+
- Standard HTTP status codes across the API
|
| 248 |
+
- Predictable error responses for clients
|
| 249 |
+
|
| 250 |
+
### 2. **Debugging**
|
| 251 |
+
- Comprehensive logging with context
|
| 252 |
+
- Request IDs for tracking
|
| 253 |
+
- Processing time metrics
|
| 254 |
+
- Full stack traces for server errors
|
| 255 |
+
|
| 256 |
+
### 3. **Security**
|
| 257 |
+
- No sensitive information in error messages
|
| 258 |
+
- Proper authentication error handling
|
| 259 |
+
- Permission checking with logging
|
| 260 |
+
|
| 261 |
+
### 4. **User Experience**
|
| 262 |
+
- Clear, actionable error messages
|
| 263 |
+
- Field-level validation feedback
|
| 264 |
+
- Helpful guidance for API consumers
|
| 265 |
+
|
| 266 |
+
### 5. **Monitoring**
|
| 267 |
+
- Request/response logging
|
| 268 |
+
- Performance metrics
|
| 269 |
+
- Error tracking capabilities
|
| 270 |
+
- Audit trail for security events
|
| 271 |
+
|
| 272 |
+
---
|
| 273 |
+
|
| 274 |
+
## Error Categories
|
| 275 |
+
|
| 276 |
+
### Client Errors (4xx)
|
| 277 |
+
- **400 Bad Request**: Invalid input, missing required fields
|
| 278 |
+
- **401 Unauthorized**: Authentication failures
|
| 279 |
+
- **403 Forbidden**: Permission denied
|
| 280 |
+
- **404 Not Found**: Resource not found
|
| 281 |
+
- **422 Unprocessable Entity**: Validation errors
|
| 282 |
+
|
| 283 |
+
### Server Errors (5xx)
|
| 284 |
+
- **500 Internal Server Error**: Unexpected errors
|
| 285 |
+
- **503 Service Unavailable**: Database connection issues
|
| 286 |
+
|
| 287 |
+
---
|
| 288 |
+
|
| 289 |
+
## Testing Recommendations
|
| 290 |
+
|
| 291 |
+
### 1. Test Authentication Errors
|
| 292 |
+
- Missing tokens
|
| 293 |
+
- Invalid tokens
|
| 294 |
+
- Expired tokens
|
| 295 |
+
- Inactive user accounts
|
| 296 |
+
|
| 297 |
+
### 2. Test Validation Errors
|
| 298 |
+
- Missing required fields
|
| 299 |
+
- Invalid email formats
|
| 300 |
+
- Short passwords
|
| 301 |
+
- Invalid data types
|
| 302 |
+
|
| 303 |
+
### 3. Test Permission Errors
|
| 304 |
+
- Non-admin accessing admin endpoints
|
| 305 |
+
- Users without required permissions
|
| 306 |
+
- Self-deactivation attempts
|
| 307 |
+
|
| 308 |
+
### 4. Test Database Errors
|
| 309 |
+
- Connection failures
|
| 310 |
+
- Operation failures
|
| 311 |
+
- Timeout scenarios
|
| 312 |
+
|
| 313 |
+
### 5. Test Edge Cases
|
| 314 |
+
- Empty strings
|
| 315 |
+
- Null values
|
| 316 |
+
- Very long inputs
|
| 317 |
+
- Special characters
|
| 318 |
+
|
| 319 |
+
---
|
| 320 |
+
|
| 321 |
+
## Documentation
|
| 322 |
+
|
| 323 |
+
Two comprehensive documentation files created:
|
| 324 |
+
|
| 325 |
+
1. **ERROR_HANDLING_GUIDE.md**
|
| 326 |
+
- Complete guide for developers
|
| 327 |
+
- Error handling patterns
|
| 328 |
+
- HTTP status codes
|
| 329 |
+
- Testing examples
|
| 330 |
+
- Best practices
|
| 331 |
+
|
| 332 |
+
2. **ERROR_HANDLING_IMPLEMENTATION_SUMMARY.md** (this file)
|
| 333 |
+
- Summary of changes
|
| 334 |
+
- Technical details
|
| 335 |
+
- Benefits and features
|
| 336 |
+
|
| 337 |
+
---
|
| 338 |
+
|
| 339 |
+
## Code Quality
|
| 340 |
+
|
| 341 |
+
### No Syntax Errors
|
| 342 |
+
All files verified with zero errors:
|
| 343 |
+
- ✅ app/main.py
|
| 344 |
+
- ✅ app/auth/controllers/router.py
|
| 345 |
+
- ✅ app/system_users/controllers/router.py
|
| 346 |
+
- ✅ app/internal/router.py
|
| 347 |
+
- ✅ app/dependencies/auth.py
|
| 348 |
+
|
| 349 |
+
### Following Best Practices
|
| 350 |
+
- Proper exception re-raising
|
| 351 |
+
- Early input validation
|
| 352 |
+
- Comprehensive logging
|
| 353 |
+
- No information leakage
|
| 354 |
+
- Type hints where appropriate
|
| 355 |
+
|
| 356 |
+
---
|
| 357 |
+
|
| 358 |
+
## Files Modified
|
| 359 |
+
|
| 360 |
+
1. `/app/main.py` - Global handlers and middleware
|
| 361 |
+
2. `/app/auth/controllers/router.py` - Auth route error handling
|
| 362 |
+
3. `/app/system_users/controllers/router.py` - User management error handling
|
| 363 |
+
4. `/app/internal/router.py` - Internal API error handling
|
| 364 |
+
5. `/app/dependencies/auth.py` - Authentication dependency error handling
|
| 365 |
+
|
| 366 |
+
## Files Created
|
| 367 |
+
|
| 368 |
+
1. `/ERROR_HANDLING_GUIDE.md` - Comprehensive error handling documentation
|
| 369 |
+
2. `/ERROR_HANDLING_IMPLEMENTATION_SUMMARY.md` - This summary document
|
| 370 |
+
|
| 371 |
+
---
|
| 372 |
+
|
| 373 |
+
## Next Steps
|
| 374 |
+
|
| 375 |
+
### Recommended Enhancements
|
| 376 |
+
|
| 377 |
+
1. **Rate Limiting**
|
| 378 |
+
- Add rate limiting middleware
|
| 379 |
+
- Protect against brute force attacks
|
| 380 |
+
- Return 429 status code
|
| 381 |
+
|
| 382 |
+
2. **Error Reporting**
|
| 383 |
+
- Integrate with error tracking service (Sentry, Rollbar)
|
| 384 |
+
- Send notifications for critical errors
|
| 385 |
+
- Create error dashboards
|
| 386 |
+
|
| 387 |
+
3. **Testing**
|
| 388 |
+
- Write unit tests for error scenarios
|
| 389 |
+
- Add integration tests
|
| 390 |
+
- Test error handler coverage
|
| 391 |
+
|
| 392 |
+
4. **Documentation**
|
| 393 |
+
- Update API documentation with error responses
|
| 394 |
+
- Add OpenAPI schema examples
|
| 395 |
+
- Create Postman collection with error cases
|
| 396 |
+
|
| 397 |
+
5. **Monitoring**
|
| 398 |
+
- Set up application monitoring
|
| 399 |
+
- Create alerts for error rates
|
| 400 |
+
- Track error patterns
|
| 401 |
+
|
| 402 |
+
---
|
| 403 |
+
|
| 404 |
+
## Conclusion
|
| 405 |
+
|
| 406 |
+
The authentication microservice now has production-ready error handling with:
|
| 407 |
+
|
| 408 |
+
✅ Comprehensive error coverage
|
| 409 |
+
✅ Consistent error responses
|
| 410 |
+
✅ Detailed logging and monitoring
|
| 411 |
+
✅ Security-aware error messages
|
| 412 |
+
✅ Developer-friendly documentation
|
| 413 |
+
✅ Performance tracking
|
| 414 |
+
✅ Request tracing capabilities
|
| 415 |
+
|
| 416 |
+
All routes are now properly protected with robust error handling that provides clear feedback to clients while maintaining security and enabling effective debugging.
|
app/auth/controllers/router.py
CHANGED
|
@@ -118,10 +118,27 @@ async def login(
|
|
| 118 |
|
| 119 |
- **email_or_phone**: User email, phone number, or username
|
| 120 |
- **password**: User password
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
"""
|
| 122 |
try:
|
| 123 |
logger.info(f"Login attempt for: {login_data.email_or_phone}")
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
# Get client IP and user agent for security tracking
|
| 126 |
client_ip = request.client.host if request.client else None
|
| 127 |
user_agent = request.headers.get("User-Agent")
|
|
@@ -145,24 +162,34 @@ async def login(
|
|
| 145 |
logger.info(f"User authenticated: {user.username}, role: {user.role}")
|
| 146 |
|
| 147 |
# Fetch permissions from SCM access roles collection based on user role
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
# Create tokens
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
# Generate accessible widgets based on user role
|
| 168 |
accessible_widgets = _get_accessible_widgets(user.role)
|
|
@@ -202,10 +229,10 @@ async def login(
|
|
| 202 |
except HTTPException:
|
| 203 |
raise
|
| 204 |
except Exception as e:
|
| 205 |
-
logger.error(f"
|
| 206 |
raise HTTPException(
|
| 207 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 208 |
-
detail="
|
| 209 |
)
|
| 210 |
|
| 211 |
|
|
@@ -224,33 +251,85 @@ async def refresh_token(
|
|
| 224 |
):
|
| 225 |
"""
|
| 226 |
Refresh access token using refresh token.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
"""
|
| 228 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
# Verify refresh token
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
raise HTTPException(
|
| 233 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 234 |
-
detail="Invalid refresh token"
|
|
|
|
| 235 |
)
|
| 236 |
|
| 237 |
user_id = payload.get("sub")
|
| 238 |
username = payload.get("username")
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
# Get user to verify they still exist and are active
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
raise HTTPException(
|
| 244 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 245 |
-
detail="User
|
| 246 |
)
|
| 247 |
|
| 248 |
# Create new access token
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
return {
|
| 256 |
"access_token": new_access_token,
|
|
@@ -261,10 +340,10 @@ async def refresh_token(
|
|
| 261 |
except HTTPException:
|
| 262 |
raise
|
| 263 |
except Exception as e:
|
| 264 |
-
logger.error(f"
|
| 265 |
raise HTTPException(
|
| 266 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 267 |
-
detail="
|
| 268 |
)
|
| 269 |
|
| 270 |
|
|
@@ -274,21 +353,37 @@ async def get_current_user_info(
|
|
| 274 |
):
|
| 275 |
"""
|
| 276 |
Get current user information.
|
|
|
|
|
|
|
|
|
|
| 277 |
"""
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
|
| 294 |
@router.post("/logout")
|
|
@@ -298,9 +393,28 @@ async def logout(
|
|
| 298 |
"""
|
| 299 |
Logout current user.
|
| 300 |
Note: In a production environment, you would want to blacklist the token.
|
|
|
|
|
|
|
|
|
|
| 301 |
"""
|
| 302 |
-
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
|
| 306 |
@router.post("/test-login")
|
|
@@ -342,12 +456,20 @@ async def get_access_roles(
|
|
| 342 |
Get available access roles and their permissions structure.
|
| 343 |
|
| 344 |
Returns the complete role hierarchy with grouped permissions.
|
|
|
|
|
|
|
|
|
|
| 345 |
"""
|
| 346 |
try:
|
| 347 |
# Get roles from database
|
| 348 |
roles = await user_service.get_all_roles()
|
| 349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
return {
|
|
|
|
| 351 |
"message": "Access roles with grouped permissions structure",
|
| 352 |
"total_roles": len(roles),
|
| 353 |
"roles": [
|
|
@@ -362,8 +484,8 @@ async def get_access_roles(
|
|
| 362 |
]
|
| 363 |
}
|
| 364 |
except Exception as e:
|
| 365 |
-
logger.error(f"Error fetching access roles: {e}")
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
"
|
| 369 |
-
|
|
|
|
| 118 |
|
| 119 |
- **email_or_phone**: User email, phone number, or username
|
| 120 |
- **password**: User password
|
| 121 |
+
|
| 122 |
+
Raises:
|
| 123 |
+
HTTPException: 401 - Invalid credentials or account locked
|
| 124 |
+
HTTPException: 500 - Database or server error
|
| 125 |
"""
|
| 126 |
try:
|
| 127 |
logger.info(f"Login attempt for: {login_data.email_or_phone}")
|
| 128 |
|
| 129 |
+
# Validate input
|
| 130 |
+
if not login_data.email_or_phone or not login_data.email_or_phone.strip():
|
| 131 |
+
raise HTTPException(
|
| 132 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 133 |
+
detail="Email, phone, or username is required"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
if not login_data.password or not login_data.password.strip():
|
| 137 |
+
raise HTTPException(
|
| 138 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 139 |
+
detail="Password is required"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
# Get client IP and user agent for security tracking
|
| 143 |
client_ip = request.client.host if request.client else None
|
| 144 |
user_agent = request.headers.get("User-Agent")
|
|
|
|
| 162 |
logger.info(f"User authenticated: {user.username}, role: {user.role}")
|
| 163 |
|
| 164 |
# Fetch permissions from SCM access roles collection based on user role
|
| 165 |
+
try:
|
| 166 |
+
scm_permissions = await user_service.get_scm_permissions_by_role(user.role)
|
| 167 |
+
|
| 168 |
+
if scm_permissions:
|
| 169 |
+
logger.info(f"SCM permissions loaded: {list(scm_permissions.keys())}")
|
| 170 |
+
else:
|
| 171 |
+
logger.warning(f"No SCM permissions found for role: {user.role}")
|
| 172 |
+
except Exception as perm_error:
|
| 173 |
+
logger.error(f"Error fetching permissions: {perm_error}")
|
| 174 |
+
scm_permissions = None
|
| 175 |
|
| 176 |
# Create tokens
|
| 177 |
+
try:
|
| 178 |
+
access_token_expires = timedelta(hours=settings.TOKEN_EXPIRATION_HOURS)
|
| 179 |
+
access_token = user_service.create_access_token(
|
| 180 |
+
data={"sub": user.user_id, "username": user.username, "role": user.role, "merchant_id": user.merchant_id, "merchant_type": user.merchant_type},
|
| 181 |
+
expires_delta=access_token_expires
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
refresh_token = user_service.create_refresh_token(
|
| 185 |
+
data={"sub": user.user_id, "username": user.username}
|
| 186 |
+
)
|
| 187 |
+
except Exception as token_error:
|
| 188 |
+
logger.error(f"Error creating tokens: {token_error}", exc_info=True)
|
| 189 |
+
raise HTTPException(
|
| 190 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 191 |
+
detail="Failed to generate authentication tokens"
|
| 192 |
+
)
|
| 193 |
|
| 194 |
# Generate accessible widgets based on user role
|
| 195 |
accessible_widgets = _get_accessible_widgets(user.role)
|
|
|
|
| 229 |
except HTTPException:
|
| 230 |
raise
|
| 231 |
except Exception as e:
|
| 232 |
+
logger.error(f"Unexpected login error for {login_data.email_or_phone}: {str(e)}", exc_info=True)
|
| 233 |
raise HTTPException(
|
| 234 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 235 |
+
detail="An unexpected error occurred during authentication"
|
| 236 |
)
|
| 237 |
|
| 238 |
|
|
|
|
| 251 |
):
|
| 252 |
"""
|
| 253 |
Refresh access token using refresh token.
|
| 254 |
+
|
| 255 |
+
Raises:
|
| 256 |
+
HTTPException: 400 - Missing or invalid refresh token
|
| 257 |
+
HTTPException: 401 - Token expired or user inactive
|
| 258 |
+
HTTPException: 500 - Server error
|
| 259 |
"""
|
| 260 |
try:
|
| 261 |
+
# Validate input
|
| 262 |
+
if not refresh_data.refresh_token or not refresh_data.refresh_token.strip():
|
| 263 |
+
raise HTTPException(
|
| 264 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 265 |
+
detail="Refresh token is required"
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
# Verify refresh token
|
| 269 |
+
try:
|
| 270 |
+
payload = user_service.verify_token(refresh_data.refresh_token, "refresh")
|
| 271 |
+
if payload is None:
|
| 272 |
+
raise HTTPException(
|
| 273 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 274 |
+
detail="Invalid or expired refresh token",
|
| 275 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 276 |
+
)
|
| 277 |
+
except Exception as verify_error:
|
| 278 |
+
logger.warning(f"Token verification failed: {verify_error}")
|
| 279 |
raise HTTPException(
|
| 280 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 281 |
+
detail="Invalid or expired refresh token",
|
| 282 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 283 |
)
|
| 284 |
|
| 285 |
user_id = payload.get("sub")
|
| 286 |
username = payload.get("username")
|
| 287 |
|
| 288 |
+
if not user_id:
|
| 289 |
+
raise HTTPException(
|
| 290 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 291 |
+
detail="Invalid token payload"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
# Get user to verify they still exist and are active
|
| 295 |
+
try:
|
| 296 |
+
user = await user_service.get_user_by_id(user_id)
|
| 297 |
+
except Exception as db_error:
|
| 298 |
+
logger.error(f"Database error fetching user {user_id}: {db_error}", exc_info=True)
|
| 299 |
+
raise HTTPException(
|
| 300 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 301 |
+
detail="Failed to verify user status"
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
if not user:
|
| 305 |
+
logger.warning(f"Token refresh attempted for non-existent user: {user_id}")
|
| 306 |
+
raise HTTPException(
|
| 307 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 308 |
+
detail="User not found"
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
if user.status.value != "active":
|
| 312 |
+
logger.warning(f"Token refresh attempted for inactive user: {user_id}, status: {user.status.value}")
|
| 313 |
raise HTTPException(
|
| 314 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 315 |
+
detail=f"User account is {user.status.value}"
|
| 316 |
)
|
| 317 |
|
| 318 |
# Create new access token
|
| 319 |
+
try:
|
| 320 |
+
access_token_expires = timedelta(hours=settings.TOKEN_EXPIRATION_HOURS)
|
| 321 |
+
new_access_token = user_service.create_access_token(
|
| 322 |
+
data={"sub": user_id, "username": username, "role": user.role, "merchant_id": user.merchant_id, "merchant_type": user.merchant_type},
|
| 323 |
+
expires_delta=access_token_expires
|
| 324 |
+
)
|
| 325 |
+
except Exception as token_error:
|
| 326 |
+
logger.error(f"Error creating new access token: {token_error}", exc_info=True)
|
| 327 |
+
raise HTTPException(
|
| 328 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 329 |
+
detail="Failed to generate new access token"
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
logger.info(f"Token refreshed successfully for user: {username}")
|
| 333 |
|
| 334 |
return {
|
| 335 |
"access_token": new_access_token,
|
|
|
|
| 340 |
except HTTPException:
|
| 341 |
raise
|
| 342 |
except Exception as e:
|
| 343 |
+
logger.error(f"Unexpected token refresh error: {str(e)}", exc_info=True)
|
| 344 |
raise HTTPException(
|
| 345 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 346 |
+
detail="An unexpected error occurred during token refresh"
|
| 347 |
)
|
| 348 |
|
| 349 |
|
|
|
|
| 353 |
):
|
| 354 |
"""
|
| 355 |
Get current user information.
|
| 356 |
+
|
| 357 |
+
Raises:
|
| 358 |
+
HTTPException: 401 - Unauthorized (invalid or missing token)
|
| 359 |
"""
|
| 360 |
+
try:
|
| 361 |
+
return {
|
| 362 |
+
"user_id": current_user.user_id,
|
| 363 |
+
"username": current_user.username,
|
| 364 |
+
"email": current_user.email,
|
| 365 |
+
"first_name": current_user.first_name,
|
| 366 |
+
"last_name": current_user.last_name,
|
| 367 |
+
"role": current_user.role,
|
| 368 |
+
"permissions": current_user.permissions,
|
| 369 |
+
"status": current_user.status.value,
|
| 370 |
+
"last_login_at": current_user.last_login_at,
|
| 371 |
+
"timezone": current_user.timezone,
|
| 372 |
+
"language": current_user.language,
|
| 373 |
+
"metadata": current_user.metadata
|
| 374 |
+
}
|
| 375 |
+
except AttributeError as e:
|
| 376 |
+
logger.error(f"Error accessing user attributes: {e}")
|
| 377 |
+
raise HTTPException(
|
| 378 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 379 |
+
detail="Error retrieving user information"
|
| 380 |
+
)
|
| 381 |
+
except Exception as e:
|
| 382 |
+
logger.error(f"Unexpected error getting current user info: {str(e)}", exc_info=True)
|
| 383 |
+
raise HTTPException(
|
| 384 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 385 |
+
detail="An unexpected error occurred"
|
| 386 |
+
)
|
| 387 |
|
| 388 |
|
| 389 |
@router.post("/logout")
|
|
|
|
| 393 |
"""
|
| 394 |
Logout current user.
|
| 395 |
Note: In a production environment, you would want to blacklist the token.
|
| 396 |
+
|
| 397 |
+
Raises:
|
| 398 |
+
HTTPException: 401 - Unauthorized (invalid or missing token)
|
| 399 |
"""
|
| 400 |
+
try:
|
| 401 |
+
logger.info(f"User logged out: {current_user.username}")
|
| 402 |
+
return {
|
| 403 |
+
"success": True,
|
| 404 |
+
"message": "Successfully logged out"
|
| 405 |
+
}
|
| 406 |
+
except AttributeError as e:
|
| 407 |
+
logger.error(f"Error accessing user during logout: {e}")
|
| 408 |
+
raise HTTPException(
|
| 409 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 410 |
+
detail="Error during logout"
|
| 411 |
+
)
|
| 412 |
+
except Exception as e:
|
| 413 |
+
logger.error(f"Unexpected logout error: {str(e)}", exc_info=True)
|
| 414 |
+
raise HTTPException(
|
| 415 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 416 |
+
detail="An unexpected error occurred during logout"
|
| 417 |
+
)
|
| 418 |
|
| 419 |
|
| 420 |
@router.post("/test-login")
|
|
|
|
| 456 |
Get available access roles and their permissions structure.
|
| 457 |
|
| 458 |
Returns the complete role hierarchy with grouped permissions.
|
| 459 |
+
|
| 460 |
+
Raises:
|
| 461 |
+
HTTPException: 500 - Database or server error
|
| 462 |
"""
|
| 463 |
try:
|
| 464 |
# Get roles from database
|
| 465 |
roles = await user_service.get_all_roles()
|
| 466 |
|
| 467 |
+
if roles is None:
|
| 468 |
+
logger.warning("get_all_roles returned None")
|
| 469 |
+
roles = []
|
| 470 |
+
|
| 471 |
return {
|
| 472 |
+
"success": True,
|
| 473 |
"message": "Access roles with grouped permissions structure",
|
| 474 |
"total_roles": len(roles),
|
| 475 |
"roles": [
|
|
|
|
| 484 |
]
|
| 485 |
}
|
| 486 |
except Exception as e:
|
| 487 |
+
logger.error(f"Error fetching access roles: {str(e)}", exc_info=True)
|
| 488 |
+
raise HTTPException(
|
| 489 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 490 |
+
detail="Failed to fetch access roles"
|
| 491 |
+
)
|
app/dependencies/auth.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Authentication dependencies for FastAPI.
|
| 3 |
"""
|
|
|
|
| 4 |
from typing import Optional
|
| 5 |
from datetime import datetime
|
| 6 |
from fastapi import Depends, HTTPException, status
|
|
@@ -9,6 +10,7 @@ from app.system_users.models.model import SystemUserModel, UserRole
|
|
| 9 |
from app.system_users.services.service import SystemUserService
|
| 10 |
from app.nosql import get_database
|
| 11 |
|
|
|
|
| 12 |
security = HTTPBearer()
|
| 13 |
|
| 14 |
|
|
@@ -18,17 +20,49 @@ def get_system_user_service() -> SystemUserService:
|
|
| 18 |
|
| 19 |
Returns:
|
| 20 |
SystemUserService: Service instance with database connection
|
|
|
|
|
|
|
|
|
|
| 21 |
"""
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
async def get_current_user(
|
| 28 |
credentials: HTTPAuthorizationCredentials = Depends(security),
|
| 29 |
user_service: SystemUserService = Depends(get_system_user_service)
|
| 30 |
) -> SystemUserModel:
|
| 31 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
credentials_exception = HTTPException(
|
| 34 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
@@ -37,28 +71,53 @@ async def get_current_user(
|
|
| 37 |
)
|
| 38 |
|
| 39 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# Verify token
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
if payload is None:
|
|
|
|
| 43 |
raise credentials_exception
|
| 44 |
|
| 45 |
user_id: str = payload.get("sub")
|
| 46 |
if user_id is None:
|
|
|
|
| 47 |
raise credentials_exception
|
| 48 |
|
| 49 |
-
except
|
|
|
|
|
|
|
|
|
|
| 50 |
raise credentials_exception
|
| 51 |
|
| 52 |
# Get user from database
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
if user is None:
|
|
|
|
| 55 |
raise credentials_exception
|
| 56 |
|
| 57 |
# Check if user is active
|
| 58 |
if user.status.value != "active":
|
|
|
|
| 59 |
raise HTTPException(
|
| 60 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 61 |
-
detail="User account is
|
| 62 |
)
|
| 63 |
|
| 64 |
return user
|
|
@@ -74,8 +133,21 @@ async def get_current_active_user(
|
|
| 74 |
async def require_admin_role(
|
| 75 |
current_user: SystemUserModel = Depends(get_current_user)
|
| 76 |
) -> SystemUserModel:
|
| 77 |
-
"""
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
raise HTTPException(
|
| 80 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 81 |
detail="Admin privileges required"
|
|
@@ -86,8 +158,21 @@ async def require_admin_role(
|
|
| 86 |
async def require_super_admin_role(
|
| 87 |
current_user: SystemUserModel = Depends(get_current_user)
|
| 88 |
) -> SystemUserModel:
|
| 89 |
-
"""
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
raise HTTPException(
|
| 92 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 93 |
detail="Super admin privileges required"
|
|
@@ -96,12 +181,27 @@ async def require_super_admin_role(
|
|
| 96 |
|
| 97 |
|
| 98 |
def require_permission(permission: str):
|
| 99 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
async def permission_checker(
|
| 101 |
current_user: SystemUserModel = Depends(get_current_user)
|
| 102 |
) -> SystemUserModel:
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
raise HTTPException(
|
| 106 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 107 |
detail=f"Permission '{permission}' required"
|
|
@@ -115,15 +215,30 @@ async def get_optional_user(
|
|
| 115 |
credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)),
|
| 116 |
user_service: SystemUserService = Depends(get_system_user_service)
|
| 117 |
) -> Optional[SystemUserModel]:
|
| 118 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
if credentials is None:
|
| 121 |
return None
|
| 122 |
|
| 123 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
# Verify token
|
| 125 |
payload = user_service.verify_token(credentials.credentials, "access")
|
| 126 |
if payload is None:
|
|
|
|
| 127 |
return None
|
| 128 |
|
| 129 |
user_id: str = payload.get("sub")
|
|
@@ -137,5 +252,6 @@ async def get_optional_user(
|
|
| 137 |
|
| 138 |
return user
|
| 139 |
|
| 140 |
-
except Exception:
|
| 141 |
-
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
Authentication dependencies for FastAPI.
|
| 3 |
"""
|
| 4 |
+
import logging
|
| 5 |
from typing import Optional
|
| 6 |
from datetime import datetime
|
| 7 |
from fastapi import Depends, HTTPException, status
|
|
|
|
| 10 |
from app.system_users.services.service import SystemUserService
|
| 11 |
from app.nosql import get_database
|
| 12 |
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
security = HTTPBearer()
|
| 15 |
|
| 16 |
|
|
|
|
| 20 |
|
| 21 |
Returns:
|
| 22 |
SystemUserService: Service instance with database connection
|
| 23 |
+
|
| 24 |
+
Raises:
|
| 25 |
+
HTTPException: 503 - Database connection not available
|
| 26 |
"""
|
| 27 |
+
try:
|
| 28 |
+
# get_database() returns AsyncIOMotorDatabase directly, no await needed
|
| 29 |
+
db = get_database()
|
| 30 |
+
if db is None:
|
| 31 |
+
logger.error("Database connection is None")
|
| 32 |
+
raise HTTPException(
|
| 33 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 34 |
+
detail="Database service unavailable"
|
| 35 |
+
)
|
| 36 |
+
return SystemUserService(db)
|
| 37 |
+
except HTTPException:
|
| 38 |
+
raise
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"Error getting system user service: {e}", exc_info=True)
|
| 41 |
+
raise HTTPException(
|
| 42 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 43 |
+
detail="Authentication service unavailable"
|
| 44 |
+
)
|
| 45 |
|
| 46 |
|
| 47 |
async def get_current_user(
|
| 48 |
credentials: HTTPAuthorizationCredentials = Depends(security),
|
| 49 |
user_service: SystemUserService = Depends(get_system_user_service)
|
| 50 |
) -> SystemUserModel:
|
| 51 |
+
"""
|
| 52 |
+
Get current authenticated user from JWT token.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
credentials: HTTP Bearer token credentials
|
| 56 |
+
user_service: System user service instance
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
SystemUserModel: The authenticated user
|
| 60 |
+
|
| 61 |
+
Raises:
|
| 62 |
+
HTTPException: 401 - Invalid or missing credentials
|
| 63 |
+
HTTPException: 403 - User account not active
|
| 64 |
+
HTTPException: 500 - Database or server error
|
| 65 |
+
"""
|
| 66 |
|
| 67 |
credentials_exception = HTTPException(
|
| 68 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
|
|
| 71 |
)
|
| 72 |
|
| 73 |
try:
|
| 74 |
+
# Validate credentials
|
| 75 |
+
if not credentials or not credentials.credentials:
|
| 76 |
+
logger.warning("Missing authentication credentials")
|
| 77 |
+
raise credentials_exception
|
| 78 |
+
|
| 79 |
# Verify token
|
| 80 |
+
try:
|
| 81 |
+
payload = user_service.verify_token(credentials.credentials, "access")
|
| 82 |
+
except Exception as token_error:
|
| 83 |
+
logger.warning(f"Token verification failed: {token_error}")
|
| 84 |
+
raise credentials_exception
|
| 85 |
+
|
| 86 |
if payload is None:
|
| 87 |
+
logger.warning("Token verification returned None")
|
| 88 |
raise credentials_exception
|
| 89 |
|
| 90 |
user_id: str = payload.get("sub")
|
| 91 |
if user_id is None:
|
| 92 |
+
logger.warning("Token payload missing 'sub' claim")
|
| 93 |
raise credentials_exception
|
| 94 |
|
| 95 |
+
except HTTPException:
|
| 96 |
+
raise
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f"Unexpected error validating credentials: {e}", exc_info=True)
|
| 99 |
raise credentials_exception
|
| 100 |
|
| 101 |
# Get user from database
|
| 102 |
+
try:
|
| 103 |
+
user = await user_service.get_user_by_id(user_id)
|
| 104 |
+
except Exception as db_error:
|
| 105 |
+
logger.error(f"Database error fetching user {user_id}: {db_error}", exc_info=True)
|
| 106 |
+
raise HTTPException(
|
| 107 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 108 |
+
detail="Failed to authenticate user"
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
if user is None:
|
| 112 |
+
logger.warning(f"User not found: {user_id}")
|
| 113 |
raise credentials_exception
|
| 114 |
|
| 115 |
# Check if user is active
|
| 116 |
if user.status.value != "active":
|
| 117 |
+
logger.warning(f"Inactive user attempted access: {user_id}, status: {user.status.value}")
|
| 118 |
raise HTTPException(
|
| 119 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 120 |
+
detail=f"User account is {user.status.value}"
|
| 121 |
)
|
| 122 |
|
| 123 |
return user
|
|
|
|
| 133 |
async def require_admin_role(
|
| 134 |
current_user: SystemUserModel = Depends(get_current_user)
|
| 135 |
) -> SystemUserModel:
|
| 136 |
+
"""
|
| 137 |
+
Require admin or super_admin role.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
current_user: The authenticated user
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
SystemUserModel: The authenticated admin user
|
| 144 |
+
|
| 145 |
+
Raises:
|
| 146 |
+
HTTPException: 403 - Insufficient privileges
|
| 147 |
+
"""
|
| 148 |
+
admin_roles = ["admin", "super_admin", "role_super_admin", "role_company_admin"]
|
| 149 |
+
if current_user.role not in admin_roles:
|
| 150 |
+
logger.warning(f"User {current_user.username} attempted admin action without privileges")
|
| 151 |
raise HTTPException(
|
| 152 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 153 |
detail="Admin privileges required"
|
|
|
|
| 158 |
async def require_super_admin_role(
|
| 159 |
current_user: SystemUserModel = Depends(get_current_user)
|
| 160 |
) -> SystemUserModel:
|
| 161 |
+
"""
|
| 162 |
+
Require super_admin role.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
current_user: The authenticated user
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
SystemUserModel: The authenticated super admin user
|
| 169 |
+
|
| 170 |
+
Raises:
|
| 171 |
+
HTTPException: 403 - Insufficient privileges
|
| 172 |
+
"""
|
| 173 |
+
super_admin_roles = ["super_admin", "role_super_admin"]
|
| 174 |
+
if current_user.role not in super_admin_roles:
|
| 175 |
+
logger.warning(f"User {current_user.username} attempted super admin action without privileges")
|
| 176 |
raise HTTPException(
|
| 177 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 178 |
detail="Super admin privileges required"
|
|
|
|
| 181 |
|
| 182 |
|
| 183 |
def require_permission(permission: str):
|
| 184 |
+
"""
|
| 185 |
+
Dependency factory to require specific permission.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
permission: The required permission string
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
Callable: Dependency function that checks for the permission
|
| 192 |
+
"""
|
| 193 |
async def permission_checker(
|
| 194 |
current_user: SystemUserModel = Depends(get_current_user)
|
| 195 |
) -> SystemUserModel:
|
| 196 |
+
# Super admins and admins have all permissions
|
| 197 |
+
if current_user.role in ["admin", "super_admin", "role_super_admin", "role_company_admin"]:
|
| 198 |
+
return current_user
|
| 199 |
+
|
| 200 |
+
# Check if user has the specific permission
|
| 201 |
+
if permission not in current_user.permissions:
|
| 202 |
+
logger.warning(
|
| 203 |
+
f"User {current_user.username} lacks required permission: {permission}"
|
| 204 |
+
)
|
| 205 |
raise HTTPException(
|
| 206 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 207 |
detail=f"Permission '{permission}' required"
|
|
|
|
| 215 |
credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)),
|
| 216 |
user_service: SystemUserService = Depends(get_system_user_service)
|
| 217 |
) -> Optional[SystemUserModel]:
|
| 218 |
+
"""
|
| 219 |
+
Get current user if token is provided, otherwise return None.
|
| 220 |
+
Useful for endpoints that work with or without authentication.
|
| 221 |
+
|
| 222 |
+
Args:
|
| 223 |
+
credentials: Optional HTTP Bearer token credentials
|
| 224 |
+
user_service: System user service instance
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
Optional[SystemUserModel]: The authenticated user or None
|
| 228 |
+
"""
|
| 229 |
|
| 230 |
if credentials is None:
|
| 231 |
return None
|
| 232 |
|
| 233 |
try:
|
| 234 |
+
# Validate credentials
|
| 235 |
+
if not credentials.credentials:
|
| 236 |
+
return None
|
| 237 |
+
|
| 238 |
# Verify token
|
| 239 |
payload = user_service.verify_token(credentials.credentials, "access")
|
| 240 |
if payload is None:
|
| 241 |
+
logger.debug("Optional token verification failed")
|
| 242 |
return None
|
| 243 |
|
| 244 |
user_id: str = payload.get("sub")
|
|
|
|
| 252 |
|
| 253 |
return user
|
| 254 |
|
| 255 |
+
except Exception as e:
|
| 256 |
+
logger.debug(f"Optional user authentication failed: {e}")
|
| 257 |
+
return None
|
app/internal/router.py
CHANGED
|
@@ -24,8 +24,43 @@ async def create_user_from_employee(
|
|
| 24 |
"""
|
| 25 |
Create a system user from employee data.
|
| 26 |
This endpoint is used by SCM service when creating employees with system access.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
"""
|
| 28 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# Generate username if not provided
|
| 30 |
username = request.username
|
| 31 |
if not username:
|
|
@@ -63,7 +98,18 @@ async def create_user_from_employee(
|
|
| 63 |
)
|
| 64 |
|
| 65 |
# Create user
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
logger.info(
|
| 69 |
f"Created system user from employee",
|
|
@@ -80,11 +126,17 @@ async def create_user_from_employee(
|
|
| 80 |
|
| 81 |
except HTTPException:
|
| 82 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
except Exception as e:
|
| 84 |
-
logger.error(f"
|
| 85 |
raise HTTPException(
|
| 86 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 87 |
-
detail="
|
| 88 |
)
|
| 89 |
|
| 90 |
|
|
@@ -96,8 +148,50 @@ async def create_user_from_merchant(
|
|
| 96 |
"""
|
| 97 |
Create a system user from merchant data.
|
| 98 |
This endpoint is used by SCM service when creating merchants with system access.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
"""
|
| 100 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
# Generate username if not provided
|
| 102 |
username = request.username
|
| 103 |
if not username:
|
|
@@ -118,6 +212,11 @@ async def create_user_from_merchant(
|
|
| 118 |
"created_via": "internal_api"
|
| 119 |
})
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
# Create user request
|
| 122 |
create_request = CreateUserRequest(
|
| 123 |
username=username,
|
|
@@ -125,15 +224,26 @@ async def create_user_from_merchant(
|
|
| 125 |
merchant_id=request.merchant_id,
|
| 126 |
merchant_type=request.merchant_type,
|
| 127 |
password=password,
|
| 128 |
-
first_name=
|
| 129 |
-
last_name=
|
| 130 |
phone=request.phone,
|
| 131 |
role=request.role_id,
|
| 132 |
metadata=metadata
|
| 133 |
)
|
| 134 |
|
| 135 |
# Create user
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
logger.info(
|
| 139 |
f"Created system user from merchant",
|
|
@@ -149,9 +259,15 @@ async def create_user_from_merchant(
|
|
| 149 |
|
| 150 |
except HTTPException:
|
| 151 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
except Exception as e:
|
| 153 |
-
logger.error(f"
|
| 154 |
raise HTTPException(
|
| 155 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 156 |
-
detail="
|
| 157 |
)
|
|
|
|
| 24 |
"""
|
| 25 |
Create a system user from employee data.
|
| 26 |
This endpoint is used by SCM service when creating employees with system access.
|
| 27 |
+
|
| 28 |
+
Raises:
|
| 29 |
+
HTTPException: 400 - Invalid data or missing required fields
|
| 30 |
+
HTTPException: 500 - Database or server error
|
| 31 |
"""
|
| 32 |
try:
|
| 33 |
+
# Validate required fields
|
| 34 |
+
if not request.employee_id or not request.employee_id.strip():
|
| 35 |
+
raise HTTPException(
|
| 36 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 37 |
+
detail="Employee ID is required"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
if not request.email or not request.email.strip():
|
| 41 |
+
raise HTTPException(
|
| 42 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 43 |
+
detail="Email is required"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
if not request.first_name or not request.first_name.strip():
|
| 47 |
+
raise HTTPException(
|
| 48 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 49 |
+
detail="First name is required"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
if not request.merchant_id or not request.merchant_id.strip():
|
| 53 |
+
raise HTTPException(
|
| 54 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 55 |
+
detail="Merchant ID is required"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
if not request.role_id or not request.role_id.strip():
|
| 59 |
+
raise HTTPException(
|
| 60 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 61 |
+
detail="Role ID is required"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
# Generate username if not provided
|
| 65 |
username = request.username
|
| 66 |
if not username:
|
|
|
|
| 98 |
)
|
| 99 |
|
| 100 |
# Create user
|
| 101 |
+
try:
|
| 102 |
+
created_user = await user_service.create_user(create_request, "system_internal")
|
| 103 |
+
except HTTPException as http_exc:
|
| 104 |
+
# Re-raise HTTPException with more context
|
| 105 |
+
logger.error(f"Failed to create user from employee {request.employee_id}: {http_exc.detail}")
|
| 106 |
+
raise
|
| 107 |
+
except Exception as create_error:
|
| 108 |
+
logger.error(f"Unexpected error creating user from employee {request.employee_id}: {create_error}", exc_info=True)
|
| 109 |
+
raise HTTPException(
|
| 110 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 111 |
+
detail="Failed to create user from employee"
|
| 112 |
+
)
|
| 113 |
|
| 114 |
logger.info(
|
| 115 |
f"Created system user from employee",
|
|
|
|
| 126 |
|
| 127 |
except HTTPException:
|
| 128 |
raise
|
| 129 |
+
except ValueError as e:
|
| 130 |
+
logger.error(f"Validation error creating user from employee {request.employee_id}: {e}")
|
| 131 |
+
raise HTTPException(
|
| 132 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 133 |
+
detail=str(e)
|
| 134 |
+
)
|
| 135 |
except Exception as e:
|
| 136 |
+
logger.error(f"Unexpected error creating user from employee: {str(e)}", exc_info=True)
|
| 137 |
raise HTTPException(
|
| 138 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 139 |
+
detail="An unexpected error occurred while creating user from employee"
|
| 140 |
)
|
| 141 |
|
| 142 |
|
|
|
|
| 148 |
"""
|
| 149 |
Create a system user from merchant data.
|
| 150 |
This endpoint is used by SCM service when creating merchants with system access.
|
| 151 |
+
|
| 152 |
+
Raises:
|
| 153 |
+
HTTPException: 400 - Invalid data or missing required fields
|
| 154 |
+
HTTPException: 500 - Database or server error
|
| 155 |
"""
|
| 156 |
try:
|
| 157 |
+
# Validate required fields
|
| 158 |
+
if not request.merchant_id or not request.merchant_id.strip():
|
| 159 |
+
raise HTTPException(
|
| 160 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 161 |
+
detail="Merchant ID is required"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
if not request.email or not request.email.strip():
|
| 165 |
+
raise HTTPException(
|
| 166 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 167 |
+
detail="Email is required"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
if not request.merchant_name or not request.merchant_name.strip():
|
| 171 |
+
raise HTTPException(
|
| 172 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 173 |
+
detail="Merchant name is required"
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
if not request.merchant_type or not request.merchant_type.strip():
|
| 177 |
+
raise HTTPException(
|
| 178 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 179 |
+
detail="Merchant type is required"
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
if not request.role_id or not request.role_id.strip():
|
| 183 |
+
raise HTTPException(
|
| 184 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 185 |
+
detail="Role ID is required"
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Validate email format
|
| 189 |
+
if "@" not in request.email:
|
| 190 |
+
raise HTTPException(
|
| 191 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 192 |
+
detail="Invalid email format"
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
# Generate username if not provided
|
| 196 |
username = request.username
|
| 197 |
if not username:
|
|
|
|
| 212 |
"created_via": "internal_api"
|
| 213 |
})
|
| 214 |
|
| 215 |
+
# Parse merchant name for first and last names
|
| 216 |
+
name_parts = request.merchant_name.split()
|
| 217 |
+
first_name = name_parts[0] if name_parts else "Merchant"
|
| 218 |
+
last_name = " ".join(name_parts[1:]) if len(name_parts) > 1 else "User"
|
| 219 |
+
|
| 220 |
# Create user request
|
| 221 |
create_request = CreateUserRequest(
|
| 222 |
username=username,
|
|
|
|
| 224 |
merchant_id=request.merchant_id,
|
| 225 |
merchant_type=request.merchant_type,
|
| 226 |
password=password,
|
| 227 |
+
first_name=first_name,
|
| 228 |
+
last_name=last_name,
|
| 229 |
phone=request.phone,
|
| 230 |
role=request.role_id,
|
| 231 |
metadata=metadata
|
| 232 |
)
|
| 233 |
|
| 234 |
# Create user
|
| 235 |
+
try:
|
| 236 |
+
created_user = await user_service.create_user(create_request, "system_internal")
|
| 237 |
+
except HTTPException as http_exc:
|
| 238 |
+
# Re-raise HTTPException with more context
|
| 239 |
+
logger.error(f"Failed to create user from merchant {request.merchant_id}: {http_exc.detail}")
|
| 240 |
+
raise
|
| 241 |
+
except Exception as create_error:
|
| 242 |
+
logger.error(f"Unexpected error creating user from merchant {request.merchant_id}: {create_error}", exc_info=True)
|
| 243 |
+
raise HTTPException(
|
| 244 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 245 |
+
detail="Failed to create user from merchant"
|
| 246 |
+
)
|
| 247 |
|
| 248 |
logger.info(
|
| 249 |
f"Created system user from merchant",
|
|
|
|
| 259 |
|
| 260 |
except HTTPException:
|
| 261 |
raise
|
| 262 |
+
except ValueError as e:
|
| 263 |
+
logger.error(f"Validation error creating user from merchant {request.merchant_id}: {e}")
|
| 264 |
+
raise HTTPException(
|
| 265 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 266 |
+
detail=str(e)
|
| 267 |
+
)
|
| 268 |
except Exception as e:
|
| 269 |
+
logger.error(f"Unexpected error creating user from merchant: {str(e)}", exc_info=True)
|
| 270 |
raise HTTPException(
|
| 271 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 272 |
+
detail="An unexpected error occurred while creating user from merchant"
|
| 273 |
)
|
app/main.py
CHANGED
|
@@ -2,9 +2,16 @@
|
|
| 2 |
Main FastAPI application for AUTH Microservice.
|
| 3 |
"""
|
| 4 |
import logging
|
|
|
|
| 5 |
from contextlib import asynccontextmanager
|
| 6 |
-
from fastapi import FastAPI
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from app.core.config import settings
|
| 9 |
|
| 10 |
from app.nosql import connect_to_mongo, close_mongo_connection
|
|
@@ -16,6 +23,23 @@ logger = logging.getLogger(__name__)
|
|
| 16 |
logging.basicConfig(level=logging.INFO)
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
@asynccontextmanager
|
| 20 |
async def lifespan(app: FastAPI):
|
| 21 |
"""Manage application lifespan events"""
|
|
@@ -43,6 +67,65 @@ app = FastAPI(
|
|
| 43 |
lifespan=lifespan
|
| 44 |
)
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
# CORS middleware
|
| 47 |
app.add_middleware(
|
| 48 |
CORSMiddleware,
|
|
@@ -53,27 +136,169 @@ app.add_middleware(
|
|
| 53 |
)
|
| 54 |
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
# Health check endpoint
|
| 57 |
@app.get("/health", tags=["health"])
|
| 58 |
async def health_check():
|
| 59 |
-
"""
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
# Debug endpoint to check database status
|
| 68 |
@app.get("/debug/db-status", tags=["debug"])
|
| 69 |
async def check_db_status():
|
| 70 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
try:
|
| 72 |
from app.nosql import get_database
|
| 73 |
from app.constants.collections import AUTH_SYSTEM_USERS_COLLECTION, AUTH_ACCESS_ROLES_COLLECTION, SCM_ACCESS_ROLES_COLLECTION
|
| 74 |
|
| 75 |
db = get_database()
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
users_count = await db[AUTH_SYSTEM_USERS_COLLECTION].count_documents({})
|
| 78 |
roles_count = await db[AUTH_ACCESS_ROLES_COLLECTION].count_documents({})
|
| 79 |
scm_roles_count = await db[SCM_ACCESS_ROLES_COLLECTION].count_documents({})
|
|
@@ -81,10 +306,11 @@ async def check_db_status():
|
|
| 81 |
# Get sample user to verify
|
| 82 |
sample_user = await db[AUTH_SYSTEM_USERS_COLLECTION].find_one(
|
| 83 |
{"email": "superadmin@cuatrolabs.com"},
|
| 84 |
-
{"email": 1, "username": 1, "role": 1, "status": 1}
|
| 85 |
)
|
| 86 |
|
| 87 |
return {
|
|
|
|
| 88 |
"database": settings.MONGODB_DB_NAME,
|
| 89 |
"collections": {
|
| 90 |
"users": users_count,
|
|
@@ -94,11 +320,14 @@ async def check_db_status():
|
|
| 94 |
"superadmin_exists": sample_user is not None,
|
| 95 |
"sample_user": sample_user if sample_user else None
|
| 96 |
}
|
|
|
|
|
|
|
| 97 |
except Exception as e:
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
# Include routers
|
|
|
|
| 2 |
Main FastAPI application for AUTH Microservice.
|
| 3 |
"""
|
| 4 |
import logging
|
| 5 |
+
import time
|
| 6 |
from contextlib import asynccontextmanager
|
| 7 |
+
from fastapi import FastAPI, Request, status
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from fastapi.responses import JSONResponse
|
| 10 |
+
from fastapi.exceptions import RequestValidationError
|
| 11 |
+
from pydantic import ValidationError, BaseModel
|
| 12 |
+
from typing import Optional, List, Dict, Any
|
| 13 |
+
from jose import JWTError
|
| 14 |
+
from pymongo.errors import PyMongoError, ConnectionFailure, OperationFailure
|
| 15 |
from app.core.config import settings
|
| 16 |
|
| 17 |
from app.nosql import connect_to_mongo, close_mongo_connection
|
|
|
|
| 23 |
logging.basicConfig(level=logging.INFO)
|
| 24 |
|
| 25 |
|
| 26 |
+
# Standard error response models
|
| 27 |
+
class ErrorDetail(BaseModel):
|
| 28 |
+
"""Detailed error information"""
|
| 29 |
+
field: Optional[str] = None
|
| 30 |
+
message: str
|
| 31 |
+
type: Optional[str] = None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class ErrorResponse(BaseModel):
|
| 35 |
+
"""Standard error response format"""
|
| 36 |
+
success: bool = False
|
| 37 |
+
error: str
|
| 38 |
+
detail: str
|
| 39 |
+
errors: Optional[List[ErrorDetail]] = None
|
| 40 |
+
request_id: Optional[str] = None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
@asynccontextmanager
|
| 44 |
async def lifespan(app: FastAPI):
|
| 45 |
"""Manage application lifespan events"""
|
|
|
|
| 67 |
lifespan=lifespan
|
| 68 |
)
|
| 69 |
|
| 70 |
+
# Request logging middleware
|
| 71 |
+
@app.middleware("http")
|
| 72 |
+
async def log_requests(request: Request, call_next):
|
| 73 |
+
"""Log all incoming requests and responses with timing."""
|
| 74 |
+
request_id = str(id(request))
|
| 75 |
+
start_time = time.time()
|
| 76 |
+
|
| 77 |
+
# Log request
|
| 78 |
+
logger.info(
|
| 79 |
+
f"Request started: {request.method} {request.url.path}",
|
| 80 |
+
extra={
|
| 81 |
+
"request_id": request_id,
|
| 82 |
+
"method": request.method,
|
| 83 |
+
"path": request.url.path,
|
| 84 |
+
"client": request.client.host if request.client else None,
|
| 85 |
+
"user_agent": request.headers.get("User-Agent", "")[:100]
|
| 86 |
+
}
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Process request
|
| 90 |
+
try:
|
| 91 |
+
response = await call_next(request)
|
| 92 |
+
|
| 93 |
+
# Calculate processing time
|
| 94 |
+
process_time = time.time() - start_time
|
| 95 |
+
|
| 96 |
+
# Log response
|
| 97 |
+
logger.info(
|
| 98 |
+
f"Request completed: {request.method} {request.url.path} - Status: {response.status_code}",
|
| 99 |
+
extra={
|
| 100 |
+
"request_id": request_id,
|
| 101 |
+
"method": request.method,
|
| 102 |
+
"path": request.url.path,
|
| 103 |
+
"status_code": response.status_code,
|
| 104 |
+
"process_time": f"{process_time:.3f}s"
|
| 105 |
+
}
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Add custom headers
|
| 109 |
+
response.headers["X-Process-Time"] = f"{process_time:.3f}"
|
| 110 |
+
response.headers["X-Request-ID"] = request_id
|
| 111 |
+
|
| 112 |
+
return response
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
process_time = time.time() - start_time
|
| 116 |
+
logger.error(
|
| 117 |
+
f"Request failed: {request.method} {request.url.path} - Error: {str(e)}",
|
| 118 |
+
exc_info=True,
|
| 119 |
+
extra={
|
| 120 |
+
"request_id": request_id,
|
| 121 |
+
"method": request.method,
|
| 122 |
+
"path": request.url.path,
|
| 123 |
+
"process_time": f"{process_time:.3f}s"
|
| 124 |
+
}
|
| 125 |
+
)
|
| 126 |
+
raise
|
| 127 |
+
|
| 128 |
+
|
| 129 |
# CORS middleware
|
| 130 |
app.add_middleware(
|
| 131 |
CORSMiddleware,
|
|
|
|
| 136 |
)
|
| 137 |
|
| 138 |
|
| 139 |
+
# Global exception handlers
|
| 140 |
+
@app.exception_handler(RequestValidationError)
|
| 141 |
+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
| 142 |
+
"""Handle request validation errors with detailed field information."""
|
| 143 |
+
errors = []
|
| 144 |
+
for error in exc.errors():
|
| 145 |
+
field = " -> ".join(str(loc) for loc in error["loc"])
|
| 146 |
+
errors.append({
|
| 147 |
+
"field": field,
|
| 148 |
+
"message": error["msg"],
|
| 149 |
+
"type": error["type"]
|
| 150 |
+
})
|
| 151 |
+
|
| 152 |
+
logger.warning(f"Validation error on {request.url.path}: {errors}")
|
| 153 |
+
|
| 154 |
+
return JSONResponse(
|
| 155 |
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 156 |
+
content={
|
| 157 |
+
"success": False,
|
| 158 |
+
"error": "Validation Error",
|
| 159 |
+
"detail": "The request contains invalid data",
|
| 160 |
+
"errors": errors
|
| 161 |
+
}
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@app.exception_handler(ValidationError)
|
| 166 |
+
async def pydantic_validation_exception_handler(request: Request, exc: ValidationError):
|
| 167 |
+
"""Handle Pydantic validation errors."""
|
| 168 |
+
logger.warning(f"Pydantic validation error on {request.url.path}: {exc.errors()}")
|
| 169 |
+
|
| 170 |
+
return JSONResponse(
|
| 171 |
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 172 |
+
content={
|
| 173 |
+
"success": False,
|
| 174 |
+
"error": "Data Validation Error",
|
| 175 |
+
"detail": str(exc),
|
| 176 |
+
"errors": exc.errors()
|
| 177 |
+
}
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@app.exception_handler(JWTError)
|
| 182 |
+
async def jwt_exception_handler(request: Request, exc: JWTError):
|
| 183 |
+
"""Handle JWT token errors."""
|
| 184 |
+
logger.warning(f"JWT error on {request.url.path}: {str(exc)}")
|
| 185 |
+
|
| 186 |
+
return JSONResponse(
|
| 187 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 188 |
+
content={
|
| 189 |
+
"success": False,
|
| 190 |
+
"error": "Authentication Error",
|
| 191 |
+
"detail": "Invalid or expired token",
|
| 192 |
+
"headers": {"WWW-Authenticate": "Bearer"}
|
| 193 |
+
}
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
@app.exception_handler(PyMongoError)
|
| 198 |
+
async def mongodb_exception_handler(request: Request, exc: PyMongoError):
|
| 199 |
+
"""Handle MongoDB errors."""
|
| 200 |
+
logger.error(f"MongoDB error on {request.url.path}: {str(exc)}", exc_info=True)
|
| 201 |
+
|
| 202 |
+
if isinstance(exc, ConnectionFailure):
|
| 203 |
+
return JSONResponse(
|
| 204 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 205 |
+
content={
|
| 206 |
+
"success": False,
|
| 207 |
+
"error": "Database Connection Error",
|
| 208 |
+
"detail": "Unable to connect to the database. Please try again later."
|
| 209 |
+
}
|
| 210 |
+
)
|
| 211 |
+
elif isinstance(exc, OperationFailure):
|
| 212 |
+
return JSONResponse(
|
| 213 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 214 |
+
content={
|
| 215 |
+
"success": False,
|
| 216 |
+
"error": "Database Operation Error",
|
| 217 |
+
"detail": "A database operation failed. Please contact support if the issue persists."
|
| 218 |
+
}
|
| 219 |
+
)
|
| 220 |
+
else:
|
| 221 |
+
return JSONResponse(
|
| 222 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 223 |
+
content={
|
| 224 |
+
"success": False,
|
| 225 |
+
"error": "Database Error",
|
| 226 |
+
"detail": "An unexpected database error occurred."
|
| 227 |
+
}
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
@app.exception_handler(Exception)
|
| 232 |
+
async def general_exception_handler(request: Request, exc: Exception):
|
| 233 |
+
"""Handle all uncaught exceptions."""
|
| 234 |
+
logger.error(
|
| 235 |
+
f"Unhandled exception on {request.method} {request.url.path}: {str(exc)}",
|
| 236 |
+
exc_info=True,
|
| 237 |
+
extra={
|
| 238 |
+
"method": request.method,
|
| 239 |
+
"path": request.url.path,
|
| 240 |
+
"client": request.client.host if request.client else None
|
| 241 |
+
}
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
return JSONResponse(
|
| 245 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 246 |
+
content={
|
| 247 |
+
"success": False,
|
| 248 |
+
"error": "Internal Server Error",
|
| 249 |
+
"detail": "An unexpected error occurred. Please try again later.",
|
| 250 |
+
"request_id": id(request) # Include request ID for tracking
|
| 251 |
+
}
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
# Health check endpoint
|
| 256 |
@app.get("/health", tags=["health"])
|
| 257 |
async def health_check():
|
| 258 |
+
"""
|
| 259 |
+
Health check endpoint.
|
| 260 |
+
Returns the service status and version.
|
| 261 |
+
"""
|
| 262 |
+
try:
|
| 263 |
+
return {
|
| 264 |
+
"status": "healthy",
|
| 265 |
+
"service": "auth-microservice",
|
| 266 |
+
"version": "1.0.0"
|
| 267 |
+
}
|
| 268 |
+
except Exception as e:
|
| 269 |
+
logger.error(f"Health check failed: {e}")
|
| 270 |
+
return JSONResponse(
|
| 271 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 272 |
+
content={
|
| 273 |
+
"status": "unhealthy",
|
| 274 |
+
"service": "auth-microservice",
|
| 275 |
+
"error": str(e)
|
| 276 |
+
}
|
| 277 |
+
)
|
| 278 |
|
| 279 |
|
| 280 |
# Debug endpoint to check database status
|
| 281 |
@app.get("/debug/db-status", tags=["debug"])
|
| 282 |
async def check_db_status():
|
| 283 |
+
"""
|
| 284 |
+
Check database connection and user count.
|
| 285 |
+
Returns information about database collections and sample data.
|
| 286 |
+
|
| 287 |
+
Raises:
|
| 288 |
+
HTTPException: 500 - Database connection error
|
| 289 |
+
"""
|
| 290 |
try:
|
| 291 |
from app.nosql import get_database
|
| 292 |
from app.constants.collections import AUTH_SYSTEM_USERS_COLLECTION, AUTH_ACCESS_ROLES_COLLECTION, SCM_ACCESS_ROLES_COLLECTION
|
| 293 |
|
| 294 |
db = get_database()
|
| 295 |
|
| 296 |
+
if db is None:
|
| 297 |
+
raise HTTPException(
|
| 298 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 299 |
+
detail="Database connection not available"
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
users_count = await db[AUTH_SYSTEM_USERS_COLLECTION].count_documents({})
|
| 303 |
roles_count = await db[AUTH_ACCESS_ROLES_COLLECTION].count_documents({})
|
| 304 |
scm_roles_count = await db[SCM_ACCESS_ROLES_COLLECTION].count_documents({})
|
|
|
|
| 306 |
# Get sample user to verify
|
| 307 |
sample_user = await db[AUTH_SYSTEM_USERS_COLLECTION].find_one(
|
| 308 |
{"email": "superadmin@cuatrolabs.com"},
|
| 309 |
+
{"email": 1, "username": 1, "role": 1, "status": 1, "_id": 0}
|
| 310 |
)
|
| 311 |
|
| 312 |
return {
|
| 313 |
+
"status": "connected",
|
| 314 |
"database": settings.MONGODB_DB_NAME,
|
| 315 |
"collections": {
|
| 316 |
"users": users_count,
|
|
|
|
| 320 |
"superadmin_exists": sample_user is not None,
|
| 321 |
"sample_user": sample_user if sample_user else None
|
| 322 |
}
|
| 323 |
+
except HTTPException:
|
| 324 |
+
raise
|
| 325 |
except Exception as e:
|
| 326 |
+
logger.error(f"Database status check failed: {str(e)}", exc_info=True)
|
| 327 |
+
raise HTTPException(
|
| 328 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 329 |
+
detail=f"Failed to check database status: {str(e)}"
|
| 330 |
+
)
|
| 331 |
|
| 332 |
|
| 333 |
# Include routers
|
app/system_users/controllers/router.py
CHANGED
|
@@ -44,21 +44,47 @@ async def login(
|
|
| 44 |
):
|
| 45 |
"""
|
| 46 |
Authenticate user and return access token.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"""
|
| 48 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
# Get client IP and user agent
|
| 50 |
client_ip = request.client.host if request.client else None
|
| 51 |
user_agent = request.headers.get("User-Agent")
|
| 52 |
|
| 53 |
# Authenticate user
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
if not user:
|
|
|
|
| 62 |
raise HTTPException(
|
| 63 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 64 |
detail=message,
|
|
@@ -66,23 +92,37 @@ async def login(
|
|
| 66 |
)
|
| 67 |
|
| 68 |
# Create access token
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
# Convert user to response model
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
logger.info(f"User logged in successfully: {user.username}")
|
| 88 |
|
|
@@ -96,10 +136,10 @@ async def login(
|
|
| 96 |
except HTTPException:
|
| 97 |
raise
|
| 98 |
except Exception as e:
|
| 99 |
-
logger.error(f"
|
| 100 |
raise HTTPException(
|
| 101 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 102 |
-
detail="
|
| 103 |
)
|
| 104 |
|
| 105 |
|
|
@@ -110,8 +150,25 @@ async def get_current_user_info(
|
|
| 110 |
):
|
| 111 |
"""
|
| 112 |
Get current user information.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
"""
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
|
| 117 |
@router.post("/users", response_model=UserInfoResponse)
|
|
@@ -122,15 +179,46 @@ async def create_user(
|
|
| 122 |
):
|
| 123 |
"""
|
| 124 |
Create a new user account. Requires admin privileges.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
"""
|
| 126 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
new_user = await user_service.create_user(user_data, current_user.user_id)
|
|
|
|
| 128 |
return user_service.convert_to_user_info_response(new_user)
|
| 129 |
|
| 130 |
except HTTPException:
|
| 131 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
except Exception as e:
|
| 133 |
-
logger.error(f"
|
| 134 |
raise HTTPException(
|
| 135 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 136 |
detail="Failed to create user"
|
|
@@ -147,9 +235,28 @@ async def list_users(
|
|
| 147 |
):
|
| 148 |
"""
|
| 149 |
List users with pagination. Requires admin privileges.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
"""
|
| 151 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
if page_size > settings.MAX_PAGE_SIZE:
|
|
|
|
| 153 |
page_size = settings.MAX_PAGE_SIZE
|
| 154 |
|
| 155 |
users, total_count = await user_service.list_users(page, page_size, status_filter)
|
|
@@ -165,8 +272,16 @@ async def list_users(
|
|
| 165 |
page_size=page_size
|
| 166 |
)
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
except Exception as e:
|
| 169 |
-
logger.error(f"
|
| 170 |
raise HTTPException(
|
| 171 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 172 |
detail="Failed to retrieve users"
|
|
@@ -201,12 +316,30 @@ async def list_users_with_projection(
|
|
| 201 |
- Reduced payload size (50-90% reduction possible)
|
| 202 |
- Better performance with field projection
|
| 203 |
- Flexible filtering options
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
"""
|
| 205 |
try:
|
| 206 |
# Validate limit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
if payload.limit > 1000:
|
|
|
|
| 208 |
payload.limit = 1000
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
# Call service with projection support
|
| 211 |
users = await user_service.list_users_with_projection(
|
| 212 |
filters=payload.filters,
|
|
@@ -239,8 +372,16 @@ async def list_users_with_projection(
|
|
| 239 |
"projection_applied": False
|
| 240 |
}
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
except Exception as e:
|
| 243 |
-
logger.error(f"
|
| 244 |
raise HTTPException(
|
| 245 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 246 |
detail="Failed to retrieve users"
|
|
@@ -255,15 +396,40 @@ async def get_user_by_id(
|
|
| 255 |
):
|
| 256 |
"""
|
| 257 |
Get user by ID. Requires admin privileges.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
"""
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
raise HTTPException(
|
| 262 |
-
status_code=status.
|
| 263 |
-
detail="
|
| 264 |
)
|
| 265 |
-
|
| 266 |
-
return user_service.convert_to_user_info_response(user)
|
| 267 |
|
| 268 |
|
| 269 |
@router.put("/users/{user_id}", response_model=UserInfoResponse)
|
|
@@ -275,21 +441,50 @@ async def update_user(
|
|
| 275 |
):
|
| 276 |
"""
|
| 277 |
Update user information. Requires admin privileges.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
"""
|
| 279 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
updated_user = await user_service.update_user(user_id, update_data, current_user.user_id)
|
|
|
|
| 281 |
if not updated_user:
|
|
|
|
| 282 |
raise HTTPException(
|
| 283 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 284 |
detail="User not found"
|
| 285 |
)
|
| 286 |
|
|
|
|
| 287 |
return user_service.convert_to_user_info_response(updated_user)
|
| 288 |
|
| 289 |
except HTTPException:
|
| 290 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
except Exception as e:
|
| 292 |
-
logger.error(f"
|
| 293 |
raise HTTPException(
|
| 294 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 295 |
detail="Failed to update user"
|
|
@@ -304,8 +499,38 @@ async def change_password(
|
|
| 304 |
):
|
| 305 |
"""
|
| 306 |
Change current user's password.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
"""
|
| 308 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
success = await user_service.change_password(
|
| 310 |
user_id=current_user.user_id,
|
| 311 |
current_password=password_data.current_password,
|
|
@@ -313,11 +538,13 @@ async def change_password(
|
|
| 313 |
)
|
| 314 |
|
| 315 |
if not success:
|
|
|
|
| 316 |
raise HTTPException(
|
| 317 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 318 |
detail="Current password is incorrect"
|
| 319 |
)
|
| 320 |
|
|
|
|
| 321 |
return StandardResponse(
|
| 322 |
success=True,
|
| 323 |
message="Password changed successfully"
|
|
@@ -326,7 +553,7 @@ async def change_password(
|
|
| 326 |
except HTTPException:
|
| 327 |
raise
|
| 328 |
except Exception as e:
|
| 329 |
-
logger.error(f"
|
| 330 |
raise HTTPException(
|
| 331 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 332 |
detail="Failed to change password"
|
|
@@ -341,22 +568,39 @@ async def deactivate_user(
|
|
| 341 |
):
|
| 342 |
"""
|
| 343 |
Deactivate user account. Requires admin privileges.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
"""
|
| 345 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
# Prevent self-deactivation
|
| 347 |
if user_id == current_user.user_id:
|
|
|
|
| 348 |
raise HTTPException(
|
| 349 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 350 |
detail="Cannot deactivate your own account"
|
| 351 |
)
|
| 352 |
|
| 353 |
success = await user_service.deactivate_user(user_id, current_user.user_id)
|
|
|
|
| 354 |
if not success:
|
|
|
|
| 355 |
raise HTTPException(
|
| 356 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 357 |
detail="User not found"
|
| 358 |
)
|
| 359 |
|
|
|
|
| 360 |
return StandardResponse(
|
| 361 |
success=True,
|
| 362 |
message="User deactivated successfully"
|
|
@@ -365,7 +609,7 @@ async def deactivate_user(
|
|
| 365 |
except HTTPException:
|
| 366 |
raise
|
| 367 |
except Exception as e:
|
| 368 |
-
logger.error(f"
|
| 369 |
raise HTTPException(
|
| 370 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 371 |
detail="Failed to deactivate user"
|
|
@@ -380,13 +624,29 @@ async def logout(
|
|
| 380 |
Logout current user.
|
| 381 |
Note: Since we're using stateless JWT tokens, actual logout would require
|
| 382 |
token blacklisting on the client side or implementing a token blacklist on server.
|
| 383 |
-
"""
|
| 384 |
-
logger.info(f"User logged out: {current_user.username}")
|
| 385 |
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
|
| 392 |
# Create default super admin endpoint (for initial setup)
|
|
@@ -397,11 +657,44 @@ async def create_super_admin(
|
|
| 397 |
):
|
| 398 |
"""
|
| 399 |
Create the first super admin user. Only works if no users exist in the system.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
"""
|
| 401 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
# Check if any users exist
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
if total_count > 0:
|
|
|
|
| 405 |
raise HTTPException(
|
| 406 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 407 |
detail="Super admin already exists or users are present in system"
|
|
@@ -419,8 +712,14 @@ async def create_super_admin(
|
|
| 419 |
|
| 420 |
except HTTPException:
|
| 421 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
except Exception as e:
|
| 423 |
-
logger.error(f"
|
| 424 |
raise HTTPException(
|
| 425 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 426 |
detail="Failed to create super admin"
|
|
|
|
| 44 |
):
|
| 45 |
"""
|
| 46 |
Authenticate user and return access token.
|
| 47 |
+
|
| 48 |
+
Raises:
|
| 49 |
+
HTTPException: 400 - Missing required fields
|
| 50 |
+
HTTPException: 401 - Invalid credentials or account locked
|
| 51 |
+
HTTPException: 500 - Database or server error
|
| 52 |
"""
|
| 53 |
try:
|
| 54 |
+
# Validate input
|
| 55 |
+
if not login_data.email_or_phone or not login_data.email_or_phone.strip():
|
| 56 |
+
raise HTTPException(
|
| 57 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 58 |
+
detail="Email, phone, or username is required"
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
if not login_data.password or not login_data.password.strip():
|
| 62 |
+
raise HTTPException(
|
| 63 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 64 |
+
detail="Password is required"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
# Get client IP and user agent
|
| 68 |
client_ip = request.client.host if request.client else None
|
| 69 |
user_agent = request.headers.get("User-Agent")
|
| 70 |
|
| 71 |
# Authenticate user
|
| 72 |
+
try:
|
| 73 |
+
user, message = await user_service.authenticate_user(
|
| 74 |
+
email_or_phone=login_data.email_or_phone,
|
| 75 |
+
password=login_data.password,
|
| 76 |
+
ip_address=client_ip,
|
| 77 |
+
user_agent=user_agent
|
| 78 |
+
)
|
| 79 |
+
except Exception as auth_error:
|
| 80 |
+
logger.error(f"Authentication error: {auth_error}", exc_info=True)
|
| 81 |
+
raise HTTPException(
|
| 82 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 83 |
+
detail="Authentication service error"
|
| 84 |
+
)
|
| 85 |
|
| 86 |
if not user:
|
| 87 |
+
logger.warning(f"Login failed for {login_data.email_or_phone}: {message}")
|
| 88 |
raise HTTPException(
|
| 89 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 90 |
detail=message,
|
|
|
|
| 92 |
)
|
| 93 |
|
| 94 |
# Create access token
|
| 95 |
+
try:
|
| 96 |
+
access_token_expires = timedelta(hours=settings.TOKEN_EXPIRATION_HOURS)
|
| 97 |
+
if login_data.remember_me:
|
| 98 |
+
access_token_expires = timedelta(hours=settings.REMEMBER_ME_TOKEN_HOURS)
|
| 99 |
+
|
| 100 |
+
access_token = user_service.create_access_token(
|
| 101 |
+
data={
|
| 102 |
+
"sub": user.user_id,
|
| 103 |
+
"username": user.username,
|
| 104 |
+
"role": user.role,
|
| 105 |
+
"merchant_id": user.merchant_id,
|
| 106 |
+
"merchant_type": user.merchant_type
|
| 107 |
+
},
|
| 108 |
+
expires_delta=access_token_expires
|
| 109 |
+
)
|
| 110 |
+
except Exception as token_error:
|
| 111 |
+
logger.error(f"Error creating token: {token_error}", exc_info=True)
|
| 112 |
+
raise HTTPException(
|
| 113 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 114 |
+
detail="Failed to generate authentication token"
|
| 115 |
+
)
|
| 116 |
|
| 117 |
# Convert user to response model
|
| 118 |
+
try:
|
| 119 |
+
user_info = user_service.convert_to_user_info_response(user)
|
| 120 |
+
except Exception as convert_error:
|
| 121 |
+
logger.error(f"Error converting user info: {convert_error}", exc_info=True)
|
| 122 |
+
raise HTTPException(
|
| 123 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 124 |
+
detail="Failed to format user information"
|
| 125 |
+
)
|
| 126 |
|
| 127 |
logger.info(f"User logged in successfully: {user.username}")
|
| 128 |
|
|
|
|
| 136 |
except HTTPException:
|
| 137 |
raise
|
| 138 |
except Exception as e:
|
| 139 |
+
logger.error(f"Unexpected login error: {str(e)}", exc_info=True)
|
| 140 |
raise HTTPException(
|
| 141 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 142 |
+
detail="An unexpected error occurred during login"
|
| 143 |
)
|
| 144 |
|
| 145 |
|
|
|
|
| 150 |
):
|
| 151 |
"""
|
| 152 |
Get current user information.
|
| 153 |
+
|
| 154 |
+
Raises:
|
| 155 |
+
HTTPException: 401 - Unauthorized (invalid or missing token)
|
| 156 |
+
HTTPException: 500 - Server error
|
| 157 |
"""
|
| 158 |
+
try:
|
| 159 |
+
return user_service.convert_to_user_info_response(current_user)
|
| 160 |
+
except AttributeError as e:
|
| 161 |
+
logger.error(f"Error accessing user attributes: {e}")
|
| 162 |
+
raise HTTPException(
|
| 163 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 164 |
+
detail="Error retrieving user information"
|
| 165 |
+
)
|
| 166 |
+
except Exception as e:
|
| 167 |
+
logger.error(f"Unexpected error getting current user info: {str(e)}", exc_info=True)
|
| 168 |
+
raise HTTPException(
|
| 169 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 170 |
+
detail="An unexpected error occurred"
|
| 171 |
+
)
|
| 172 |
|
| 173 |
|
| 174 |
@router.post("/users", response_model=UserInfoResponse)
|
|
|
|
| 179 |
):
|
| 180 |
"""
|
| 181 |
Create a new user account. Requires admin privileges.
|
| 182 |
+
|
| 183 |
+
Raises:
|
| 184 |
+
HTTPException: 400 - Invalid data or user already exists
|
| 185 |
+
HTTPException: 403 - Insufficient permissions
|
| 186 |
+
HTTPException: 500 - Database or server error
|
| 187 |
"""
|
| 188 |
try:
|
| 189 |
+
# Additional validation
|
| 190 |
+
if not user_data.username or not user_data.username.strip():
|
| 191 |
+
raise HTTPException(
|
| 192 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 193 |
+
detail="Username is required"
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
if not user_data.email or not user_data.email.strip():
|
| 197 |
+
raise HTTPException(
|
| 198 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 199 |
+
detail="Email is required"
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
if not user_data.password or len(user_data.password) < 8:
|
| 203 |
+
raise HTTPException(
|
| 204 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 205 |
+
detail="Password must be at least 8 characters long"
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
new_user = await user_service.create_user(user_data, current_user.user_id)
|
| 209 |
+
logger.info(f"User created successfully by {current_user.username}: {new_user.username}")
|
| 210 |
return user_service.convert_to_user_info_response(new_user)
|
| 211 |
|
| 212 |
except HTTPException:
|
| 213 |
raise
|
| 214 |
+
except ValueError as e:
|
| 215 |
+
logger.error(f"Validation error creating user: {e}")
|
| 216 |
+
raise HTTPException(
|
| 217 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 218 |
+
detail=str(e)
|
| 219 |
+
)
|
| 220 |
except Exception as e:
|
| 221 |
+
logger.error(f"Unexpected error creating user: {str(e)}", exc_info=True)
|
| 222 |
raise HTTPException(
|
| 223 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 224 |
detail="Failed to create user"
|
|
|
|
| 235 |
):
|
| 236 |
"""
|
| 237 |
List users with pagination. Requires admin privileges.
|
| 238 |
+
|
| 239 |
+
Raises:
|
| 240 |
+
HTTPException: 400 - Invalid pagination parameters
|
| 241 |
+
HTTPException: 403 - Insufficient permissions
|
| 242 |
+
HTTPException: 500 - Database or server error
|
| 243 |
"""
|
| 244 |
try:
|
| 245 |
+
# Validate pagination parameters
|
| 246 |
+
if page < 1:
|
| 247 |
+
raise HTTPException(
|
| 248 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 249 |
+
detail="Page number must be greater than 0"
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
if page_size < 1:
|
| 253 |
+
raise HTTPException(
|
| 254 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 255 |
+
detail="Page size must be greater than 0"
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
if page_size > settings.MAX_PAGE_SIZE:
|
| 259 |
+
logger.info(f"Page size {page_size} exceeds max, setting to {settings.MAX_PAGE_SIZE}")
|
| 260 |
page_size = settings.MAX_PAGE_SIZE
|
| 261 |
|
| 262 |
users, total_count = await user_service.list_users(page, page_size, status_filter)
|
|
|
|
| 272 |
page_size=page_size
|
| 273 |
)
|
| 274 |
|
| 275 |
+
except HTTPException:
|
| 276 |
+
raise
|
| 277 |
+
except ValueError as e:
|
| 278 |
+
logger.error(f"Validation error listing users: {e}")
|
| 279 |
+
raise HTTPException(
|
| 280 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 281 |
+
detail=str(e)
|
| 282 |
+
)
|
| 283 |
except Exception as e:
|
| 284 |
+
logger.error(f"Unexpected error listing users: {str(e)}", exc_info=True)
|
| 285 |
raise HTTPException(
|
| 286 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 287 |
detail="Failed to retrieve users"
|
|
|
|
| 316 |
- Reduced payload size (50-90% reduction possible)
|
| 317 |
- Better performance with field projection
|
| 318 |
- Flexible filtering options
|
| 319 |
+
|
| 320 |
+
Raises:
|
| 321 |
+
HTTPException: 400 - Invalid parameters
|
| 322 |
+
HTTPException: 403 - Insufficient permissions
|
| 323 |
+
HTTPException: 500 - Database or server error
|
| 324 |
"""
|
| 325 |
try:
|
| 326 |
# Validate limit
|
| 327 |
+
if payload.limit < 1:
|
| 328 |
+
raise HTTPException(
|
| 329 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 330 |
+
detail="Limit must be greater than 0"
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
if payload.limit > 1000:
|
| 334 |
+
logger.info(f"Limit {payload.limit} exceeds max 1000, setting to 1000")
|
| 335 |
payload.limit = 1000
|
| 336 |
|
| 337 |
+
if payload.skip < 0:
|
| 338 |
+
raise HTTPException(
|
| 339 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 340 |
+
detail="Skip must be 0 or greater"
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
# Call service with projection support
|
| 344 |
users = await user_service.list_users_with_projection(
|
| 345 |
filters=payload.filters,
|
|
|
|
| 372 |
"projection_applied": False
|
| 373 |
}
|
| 374 |
|
| 375 |
+
except HTTPException:
|
| 376 |
+
raise
|
| 377 |
+
except ValueError as e:
|
| 378 |
+
logger.error(f"Validation error in list_users_with_projection: {e}")
|
| 379 |
+
raise HTTPException(
|
| 380 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 381 |
+
detail=str(e)
|
| 382 |
+
)
|
| 383 |
except Exception as e:
|
| 384 |
+
logger.error(f"Unexpected error listing users with projection: {str(e)}", exc_info=True)
|
| 385 |
raise HTTPException(
|
| 386 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 387 |
detail="Failed to retrieve users"
|
|
|
|
| 396 |
):
|
| 397 |
"""
|
| 398 |
Get user by ID. Requires admin privileges.
|
| 399 |
+
|
| 400 |
+
Raises:
|
| 401 |
+
HTTPException: 400 - Invalid user ID
|
| 402 |
+
HTTPException: 403 - Insufficient permissions
|
| 403 |
+
HTTPException: 404 - User not found
|
| 404 |
+
HTTPException: 500 - Database or server error
|
| 405 |
"""
|
| 406 |
+
try:
|
| 407 |
+
# Validate user_id
|
| 408 |
+
if not user_id or not user_id.strip():
|
| 409 |
+
raise HTTPException(
|
| 410 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 411 |
+
detail="User ID is required"
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
user = await user_service.get_user_by_id(user_id)
|
| 415 |
+
|
| 416 |
+
if not user:
|
| 417 |
+
logger.warning(f"User not found: {user_id}")
|
| 418 |
+
raise HTTPException(
|
| 419 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 420 |
+
detail="User not found"
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
return user_service.convert_to_user_info_response(user)
|
| 424 |
+
|
| 425 |
+
except HTTPException:
|
| 426 |
+
raise
|
| 427 |
+
except Exception as e:
|
| 428 |
+
logger.error(f"Unexpected error getting user {user_id}: {str(e)}", exc_info=True)
|
| 429 |
raise HTTPException(
|
| 430 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 431 |
+
detail="Failed to retrieve user"
|
| 432 |
)
|
|
|
|
|
|
|
| 433 |
|
| 434 |
|
| 435 |
@router.put("/users/{user_id}", response_model=UserInfoResponse)
|
|
|
|
| 441 |
):
|
| 442 |
"""
|
| 443 |
Update user information. Requires admin privileges.
|
| 444 |
+
|
| 445 |
+
Raises:
|
| 446 |
+
HTTPException: 400 - Invalid data or user ID
|
| 447 |
+
HTTPException: 403 - Insufficient permissions
|
| 448 |
+
HTTPException: 404 - User not found
|
| 449 |
+
HTTPException: 500 - Database or server error
|
| 450 |
"""
|
| 451 |
try:
|
| 452 |
+
# Validate user_id
|
| 453 |
+
if not user_id or not user_id.strip():
|
| 454 |
+
raise HTTPException(
|
| 455 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 456 |
+
detail="User ID is required"
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
# Check if any data to update
|
| 460 |
+
if not update_data.dict(exclude_unset=True):
|
| 461 |
+
raise HTTPException(
|
| 462 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 463 |
+
detail="No data provided for update"
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
updated_user = await user_service.update_user(user_id, update_data, current_user.user_id)
|
| 467 |
+
|
| 468 |
if not updated_user:
|
| 469 |
+
logger.warning(f"User not found for update: {user_id}")
|
| 470 |
raise HTTPException(
|
| 471 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 472 |
detail="User not found"
|
| 473 |
)
|
| 474 |
|
| 475 |
+
logger.info(f"User {user_id} updated by {current_user.username}")
|
| 476 |
return user_service.convert_to_user_info_response(updated_user)
|
| 477 |
|
| 478 |
except HTTPException:
|
| 479 |
raise
|
| 480 |
+
except ValueError as e:
|
| 481 |
+
logger.error(f"Validation error updating user {user_id}: {e}")
|
| 482 |
+
raise HTTPException(
|
| 483 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 484 |
+
detail=str(e)
|
| 485 |
+
)
|
| 486 |
except Exception as e:
|
| 487 |
+
logger.error(f"Unexpected error updating user {user_id}: {str(e)}", exc_info=True)
|
| 488 |
raise HTTPException(
|
| 489 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 490 |
detail="Failed to update user"
|
|
|
|
| 499 |
):
|
| 500 |
"""
|
| 501 |
Change current user's password.
|
| 502 |
+
|
| 503 |
+
Raises:
|
| 504 |
+
HTTPException: 400 - Invalid password or missing fields
|
| 505 |
+
HTTPException: 401 - Current password incorrect
|
| 506 |
+
HTTPException: 500 - Database or server error
|
| 507 |
"""
|
| 508 |
try:
|
| 509 |
+
# Validate passwords
|
| 510 |
+
if not password_data.current_password or not password_data.current_password.strip():
|
| 511 |
+
raise HTTPException(
|
| 512 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 513 |
+
detail="Current password is required"
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
if not password_data.new_password or not password_data.new_password.strip():
|
| 517 |
+
raise HTTPException(
|
| 518 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 519 |
+
detail="New password is required"
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
if len(password_data.new_password) < 8:
|
| 523 |
+
raise HTTPException(
|
| 524 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 525 |
+
detail="New password must be at least 8 characters long"
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
if password_data.current_password == password_data.new_password:
|
| 529 |
+
raise HTTPException(
|
| 530 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 531 |
+
detail="New password must be different from current password"
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
success = await user_service.change_password(
|
| 535 |
user_id=current_user.user_id,
|
| 536 |
current_password=password_data.current_password,
|
|
|
|
| 538 |
)
|
| 539 |
|
| 540 |
if not success:
|
| 541 |
+
logger.warning(f"Failed password change attempt for user {current_user.user_id}")
|
| 542 |
raise HTTPException(
|
| 543 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 544 |
detail="Current password is incorrect"
|
| 545 |
)
|
| 546 |
|
| 547 |
+
logger.info(f"Password changed successfully for user {current_user.username}")
|
| 548 |
return StandardResponse(
|
| 549 |
success=True,
|
| 550 |
message="Password changed successfully"
|
|
|
|
| 553 |
except HTTPException:
|
| 554 |
raise
|
| 555 |
except Exception as e:
|
| 556 |
+
logger.error(f"Unexpected error changing password for user {current_user.user_id}: {str(e)}", exc_info=True)
|
| 557 |
raise HTTPException(
|
| 558 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 559 |
detail="Failed to change password"
|
|
|
|
| 568 |
):
|
| 569 |
"""
|
| 570 |
Deactivate user account. Requires admin privileges.
|
| 571 |
+
|
| 572 |
+
Raises:
|
| 573 |
+
HTTPException: 400 - Cannot deactivate own account or invalid user ID
|
| 574 |
+
HTTPException: 403 - Insufficient permissions
|
| 575 |
+
HTTPException: 404 - User not found
|
| 576 |
+
HTTPException: 500 - Database or server error
|
| 577 |
"""
|
| 578 |
try:
|
| 579 |
+
# Validate user_id
|
| 580 |
+
if not user_id or not user_id.strip():
|
| 581 |
+
raise HTTPException(
|
| 582 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 583 |
+
detail="User ID is required"
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
# Prevent self-deactivation
|
| 587 |
if user_id == current_user.user_id:
|
| 588 |
+
logger.warning(f"User {current_user.username} attempted to deactivate their own account")
|
| 589 |
raise HTTPException(
|
| 590 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 591 |
detail="Cannot deactivate your own account"
|
| 592 |
)
|
| 593 |
|
| 594 |
success = await user_service.deactivate_user(user_id, current_user.user_id)
|
| 595 |
+
|
| 596 |
if not success:
|
| 597 |
+
logger.warning(f"User not found for deactivation: {user_id}")
|
| 598 |
raise HTTPException(
|
| 599 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 600 |
detail="User not found"
|
| 601 |
)
|
| 602 |
|
| 603 |
+
logger.info(f"User {user_id} deactivated by {current_user.username}")
|
| 604 |
return StandardResponse(
|
| 605 |
success=True,
|
| 606 |
message="User deactivated successfully"
|
|
|
|
| 609 |
except HTTPException:
|
| 610 |
raise
|
| 611 |
except Exception as e:
|
| 612 |
+
logger.error(f"Unexpected error deactivating user {user_id}: {str(e)}", exc_info=True)
|
| 613 |
raise HTTPException(
|
| 614 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 615 |
detail="Failed to deactivate user"
|
|
|
|
| 624 |
Logout current user.
|
| 625 |
Note: Since we're using stateless JWT tokens, actual logout would require
|
| 626 |
token blacklisting on the client side or implementing a token blacklist on server.
|
|
|
|
|
|
|
| 627 |
|
| 628 |
+
Raises:
|
| 629 |
+
HTTPException: 401 - Unauthorized (invalid or missing token)
|
| 630 |
+
"""
|
| 631 |
+
try:
|
| 632 |
+
logger.info(f"User logged out: {current_user.username}")
|
| 633 |
+
|
| 634 |
+
return StandardResponse(
|
| 635 |
+
success=True,
|
| 636 |
+
message="Logged out successfully"
|
| 637 |
+
)
|
| 638 |
+
except AttributeError as e:
|
| 639 |
+
logger.error(f"Error accessing user during logout: {e}")
|
| 640 |
+
raise HTTPException(
|
| 641 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 642 |
+
detail="Error during logout"
|
| 643 |
+
)
|
| 644 |
+
except Exception as e:
|
| 645 |
+
logger.error(f"Unexpected logout error: {str(e)}", exc_info=True)
|
| 646 |
+
raise HTTPException(
|
| 647 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 648 |
+
detail="An unexpected error occurred during logout"
|
| 649 |
+
)
|
| 650 |
|
| 651 |
|
| 652 |
# Create default super admin endpoint (for initial setup)
|
|
|
|
| 657 |
):
|
| 658 |
"""
|
| 659 |
Create the first super admin user. Only works if no users exist in the system.
|
| 660 |
+
|
| 661 |
+
Raises:
|
| 662 |
+
HTTPException: 400 - Invalid data
|
| 663 |
+
HTTPException: 403 - Super admin already exists
|
| 664 |
+
HTTPException: 500 - Database or server error
|
| 665 |
"""
|
| 666 |
try:
|
| 667 |
+
# Validate required fields
|
| 668 |
+
if not user_data.username or not user_data.username.strip():
|
| 669 |
+
raise HTTPException(
|
| 670 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 671 |
+
detail="Username is required"
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
if not user_data.email or not user_data.email.strip():
|
| 675 |
+
raise HTTPException(
|
| 676 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 677 |
+
detail="Email is required"
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
if not user_data.password or len(user_data.password) < 8:
|
| 681 |
+
raise HTTPException(
|
| 682 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 683 |
+
detail="Password must be at least 8 characters long"
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
# Check if any users exist
|
| 687 |
+
try:
|
| 688 |
+
users, total_count = await user_service.list_users(page=1, page_size=1)
|
| 689 |
+
except Exception as db_error:
|
| 690 |
+
logger.error(f"Database error checking existing users: {db_error}", exc_info=True)
|
| 691 |
+
raise HTTPException(
|
| 692 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 693 |
+
detail="Failed to verify system state"
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
if total_count > 0:
|
| 697 |
+
logger.warning("Attempted to create super admin when users already exist")
|
| 698 |
raise HTTPException(
|
| 699 |
status_code=status.HTTP_403_FORBIDDEN,
|
| 700 |
detail="Super admin already exists or users are present in system"
|
|
|
|
| 712 |
|
| 713 |
except HTTPException:
|
| 714 |
raise
|
| 715 |
+
except ValueError as e:
|
| 716 |
+
logger.error(f"Validation error creating super admin: {e}")
|
| 717 |
+
raise HTTPException(
|
| 718 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 719 |
+
detail=str(e)
|
| 720 |
+
)
|
| 721 |
except Exception as e:
|
| 722 |
+
logger.error(f"Unexpected error creating super admin: {str(e)}", exc_info=True)
|
| 723 |
raise HTTPException(
|
| 724 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 725 |
detail="Failed to create super admin"
|