MukeshKapoor25 commited on
Commit
7a5b513
·
1 Parent(s): bf0de9d

feat: Implement System Users module for authentication and user management

Browse files

- Added `system_users` package with controllers, models, schemas, and services.
- Created API endpoints for user login, registration, user management, and password change.
- Implemented JWT authentication and user role management.
- Added database management utilities for creating indexes and default roles.
- Updated requirements.txt with necessary dependencies for FastAPI, MongoDB, and JWT handling.
- Created a startup script to set up the environment and run the FastAPI server.

.env ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Auth Microservice Environment Configuration
2
+
3
+ # Application Settings
4
+ APP_NAME=Auth Microservice
5
+ APP_VERSION=1.0.0
6
+ DEBUG=false
7
+
8
+ # MongoDB Configuration
9
+ MONGODB_URI=mongodb://localhost:27017
10
+ MONGODB_DB_NAME=auth_db
11
+
12
+ # Redis Configuration (for caching and session management)
13
+ REDIS_HOST=localhost
14
+ REDIS_PORT=6379
15
+ REDIS_PASSWORD=
16
+ REDIS_DB=0
17
+
18
+ # JWT Configuration
19
+ SECRET_KEY=your-super-secret-jwt-key-change-in-production-please-make-it-very-long-and-random
20
+ ALGORITHM=HS256
21
+ TOKEN_EXPIRATION_HOURS=8
22
+
23
+ # OTP Configuration
24
+ OTP_TTL_SECONDS=600
25
+ OTP_RATE_LIMIT_MAX=10
26
+ OTP_RATE_LIMIT_WINDOW=600
27
+
28
+ # Twilio Configuration (for SMS OTP)
29
+ TWILIO_ACCOUNT_SID=
30
+ TWILIO_AUTH_TOKEN=
31
+ TWILIO_PHONE_NUMBER=
32
+
33
+ # SMTP Configuration (for email notifications)
34
+ SMTP_HOST=
35
+ SMTP_PORT=587
36
+ SMTP_USERNAME=
37
+ SMTP_PASSWORD=
38
+ SMTP_FROM_EMAIL=
39
+ SMTP_USE_TLS=true
40
+
41
+ # Logging Configuration
42
+ LOG_LEVEL=INFO
43
+
44
+ # CORS Settings
45
+ CORS_ORIGINS=["http://localhost:3000","http://localhost:8000","http://localhost:8002"]
46
+
app/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Auth Microservice Application Package
3
+ """
app/auth/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Authentication module for Auth microservice
3
+ """
app/auth/controllers/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Authentication controllers module
3
+ """
app/auth/controllers/router.py ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication router for login, logout, and token management endpoints.
3
+ Provides JWT-based authentication with enhanced security features.
4
+ """
5
+ from datetime import timedelta
6
+ from typing import Optional, List, Dict
7
+ from fastapi import APIRouter, Depends, HTTPException, status, Body, Request
8
+ from pydantic import BaseModel, EmailStr
9
+ import logging
10
+
11
+ from app.system_users.services.service import SystemUserService
12
+ from app.dependencies.auth import get_system_user_service, get_current_user
13
+ from app.system_users.models.model import SystemUserModel
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ router = APIRouter(prefix="/auth", tags=["Authentication"])
18
+
19
+
20
+ def _get_accessible_widgets(user_role) -> List[Dict]:
21
+ """Generate accessible widgets based on user role for authentication system."""
22
+ # Base widgets available to all roles - Authentication focused
23
+ base_widgets = [
24
+ {
25
+ "widget_id": "wid_login_count_001",
26
+ "widget_type": "kpi",
27
+ "title": "Login Count",
28
+ "accessible": True
29
+ },
30
+ {
31
+ "widget_id": "wid_active_users_001",
32
+ "widget_type": "kpi",
33
+ "title": "Active Users",
34
+ "accessible": True
35
+ },
36
+ {
37
+ "widget_id": "wid_failed_logins_001",
38
+ "widget_type": "kpi",
39
+ "title": "Failed Logins (24h)",
40
+ "accessible": True
41
+ },
42
+ {
43
+ "widget_id": "wid_user_roles_001",
44
+ "widget_type": "chart",
45
+ "title": "User Roles Distribution",
46
+ "accessible": True
47
+ },
48
+ {
49
+ "widget_id": "wid_login_trend_001",
50
+ "widget_type": "chart",
51
+ "title": "Login Trend (7 days)",
52
+ "accessible": True
53
+ }
54
+ ]
55
+
56
+ # Advanced widgets for managers and above
57
+ advanced_widgets = [
58
+ {
59
+ "widget_id": "wid_security_events_001",
60
+ "widget_type": "table",
61
+ "title": "Recent Security Events",
62
+ "accessible": True
63
+ },
64
+ {
65
+ "widget_id": "wid_locked_accounts_001",
66
+ "widget_type": "table",
67
+ "title": "Locked Accounts",
68
+ "accessible": True
69
+ },
70
+ {
71
+ "widget_id": "wid_recent_registrations_001",
72
+ "widget_type": "table",
73
+ "title": "Recent User Registrations",
74
+ "accessible": True
75
+ }
76
+ ]
77
+
78
+ # Return widgets based on role
79
+ if user_role.value in ["super_admin", "admin"]:
80
+ return base_widgets + advanced_widgets
81
+ elif user_role.value in ["manager"]:
82
+ return base_widgets + advanced_widgets[:2] # Limited advanced widgets
83
+ else:
84
+ return base_widgets # Basic widgets only
85
+
86
+
87
+ class LoginRequest(BaseModel):
88
+ """Login request model."""
89
+ email_or_phone: str # Can be email, phone number, or username
90
+ password: str
91
+
92
+
93
+ class LoginResponse(BaseModel):
94
+ """Login response model."""
95
+ access_token: str
96
+ refresh_token: str
97
+ token_type: str = "bearer"
98
+ expires_in: int = 1800 # 30 minutes
99
+ user: dict
100
+ access_menu: dict
101
+ warnings: Optional[str] = None
102
+
103
+
104
+ class TokenRefreshRequest(BaseModel):
105
+ """Token refresh request."""
106
+ refresh_token: str
107
+
108
+
109
+ @router.post("/login", response_model=LoginResponse)
110
+ async def login(
111
+ request: Request,
112
+ login_data: LoginRequest,
113
+ user_service: SystemUserService = Depends(get_system_user_service)
114
+ ):
115
+ """
116
+ Authenticate user and return JWT tokens.
117
+
118
+ - **email_or_phone**: User email, phone number, or username
119
+ - **password**: User password
120
+ """
121
+ try:
122
+ # Get client IP and user agent for security tracking
123
+ client_ip = request.client.host if request.client else None
124
+ user_agent = request.headers.get("User-Agent")
125
+
126
+ # Authenticate user
127
+ user, message = await user_service.authenticate_user(
128
+ login_data.email_or_phone,
129
+ login_data.password,
130
+ ip_address=client_ip,
131
+ user_agent=user_agent
132
+ )
133
+
134
+ if not user:
135
+ raise HTTPException(
136
+ status_code=status.HTTP_401_UNAUTHORIZED,
137
+ detail=message,
138
+ headers={"WWW-Authenticate": "Bearer"}
139
+ )
140
+
141
+ # Create tokens
142
+ access_token_expires = timedelta(minutes=30)
143
+ access_token = user_service.create_access_token(
144
+ data={"sub": user.user_id, "username": user.username, "role": user.role.value},
145
+ expires_delta=access_token_expires
146
+ )
147
+
148
+ refresh_token = user_service.create_refresh_token(
149
+ data={"sub": user.user_id, "username": user.username}
150
+ )
151
+
152
+ # Flatten permissions to dot notation
153
+ flattened_permissions = []
154
+ for module, actions in user.permissions.items():
155
+ for action in actions:
156
+ flattened_permissions.append(f"{module}.{action}")
157
+
158
+ # Generate accessible widgets based on user role
159
+ accessible_widgets = _get_accessible_widgets(user.role)
160
+
161
+ # Return user info without sensitive data
162
+ user_info = {
163
+ "user_id": user.user_id,
164
+ "username": user.username,
165
+ "email": user.email,
166
+ "first_name": user.first_name,
167
+ "last_name": user.last_name,
168
+ "role": user.role.value,
169
+ "permissions": user.permissions,
170
+ "status": user.status.value,
171
+ "last_login_at": user.last_login_at,
172
+ "metadata": user.metadata
173
+ }
174
+
175
+ # Access menu structure
176
+ access_menu = {
177
+ "permissions": flattened_permissions,
178
+ "accessible_widgets": accessible_widgets
179
+ }
180
+
181
+ logger.info(f"User logged in successfully: {user.username}")
182
+
183
+ return LoginResponse(
184
+ access_token=access_token,
185
+ refresh_token=refresh_token,
186
+ user=user_info,
187
+ access_menu=access_menu,
188
+ warnings=None
189
+ )
190
+
191
+ except HTTPException:
192
+ raise
193
+ except Exception as e:
194
+ logger.error(f"Login error: {e}")
195
+ raise HTTPException(
196
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
197
+ detail="Authentication failed"
198
+ )
199
+
200
+
201
+ class OAuth2LoginRequest(BaseModel):
202
+ """OAuth2 compatible login request."""
203
+ username: str # Can be email or phone
204
+ password: str
205
+ grant_type: str = "password"
206
+
207
+
208
+ @router.post("/login-form")
209
+ async def login_form(
210
+ request: Request,
211
+ form_data: OAuth2LoginRequest,
212
+ user_service: SystemUserService = Depends(get_system_user_service)
213
+ ):
214
+ """
215
+ OAuth2 compatible login endpoint for form-based authentication.
216
+ """
217
+ try:
218
+ # Get client IP and user agent
219
+ client_ip = request.client.host if request.client else None
220
+ user_agent = request.headers.get("User-Agent")
221
+
222
+ # Authenticate user
223
+ user, message = await user_service.authenticate_user(
224
+ form_data.username, # Can be email or phone
225
+ form_data.password,
226
+ ip_address=client_ip,
227
+ user_agent=user_agent
228
+ )
229
+
230
+ if not user:
231
+ raise HTTPException(
232
+ status_code=status.HTTP_401_UNAUTHORIZED,
233
+ detail=message,
234
+ headers={"WWW-Authenticate": "Bearer"}
235
+ )
236
+
237
+ # Create access token
238
+ access_token_expires = timedelta(minutes=30)
239
+ access_token = user_service.create_access_token(
240
+ data={"sub": user.user_id, "username": user.username, "role": user.role.value},
241
+ expires_delta=access_token_expires
242
+ )
243
+
244
+ return {
245
+ "access_token": access_token,
246
+ "token_type": "bearer"
247
+ }
248
+
249
+ except HTTPException:
250
+ raise
251
+ except Exception as e:
252
+ logger.error(f"Form login error: {e}")
253
+ raise HTTPException(
254
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
255
+ detail="Authentication failed"
256
+ )
257
+
258
+
259
+ @router.post("/refresh")
260
+ async def refresh_token(
261
+ refresh_data: TokenRefreshRequest,
262
+ user_service: SystemUserService = Depends(get_system_user_service)
263
+ ):
264
+ """
265
+ Refresh access token using refresh token.
266
+ """
267
+ try:
268
+ # Verify refresh token
269
+ payload = user_service.verify_token(refresh_data.refresh_token, "refresh")
270
+ if payload is None:
271
+ raise HTTPException(
272
+ status_code=status.HTTP_401_UNAUTHORIZED,
273
+ detail="Invalid refresh token"
274
+ )
275
+
276
+ user_id = payload.get("sub")
277
+ username = payload.get("username")
278
+
279
+ # Get user to verify they still exist and are active
280
+ user = await user_service.get_user_by_id(user_id)
281
+ if not user or user.status.value != "active":
282
+ raise HTTPException(
283
+ status_code=status.HTTP_401_UNAUTHORIZED,
284
+ detail="User not found or inactive"
285
+ )
286
+
287
+ # Create new access token
288
+ access_token_expires = timedelta(minutes=30)
289
+ access_token = user_service.create_access_token(
290
+ data={"sub": user_id, "username": username, "role": user.role.value},
291
+ expires_delta=access_token_expires
292
+ )
293
+
294
+ return {
295
+ "access_token": access_token,
296
+ "token_type": "bearer",
297
+ "expires_in": 1800
298
+ }
299
+
300
+ except HTTPException:
301
+ raise
302
+ except Exception as e:
303
+ logger.error(f"Token refresh error: {e}")
304
+ raise HTTPException(
305
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
306
+ detail="Token refresh failed"
307
+ )
308
+
309
+
310
+ @router.get("/me")
311
+ async def get_current_user_info(
312
+ current_user: SystemUserModel = Depends(get_current_user)
313
+ ):
314
+ """
315
+ Get current user information.
316
+ """
317
+ return {
318
+ "user_id": current_user.user_id,
319
+ "username": current_user.username,
320
+ "email": current_user.email,
321
+ "first_name": current_user.first_name,
322
+ "last_name": current_user.last_name,
323
+ "role": current_user.role.value,
324
+ "permissions": current_user.permissions,
325
+ "status": current_user.status.value,
326
+ "last_login_at": current_user.last_login_at,
327
+ "timezone": current_user.timezone,
328
+ "language": current_user.language,
329
+ "metadata": current_user.metadata
330
+ }
331
+
332
+
333
+ @router.post("/logout")
334
+ async def logout(
335
+ current_user: SystemUserModel = Depends(get_current_user)
336
+ ):
337
+ """
338
+ Logout current user.
339
+ Note: In a production environment, you would want to blacklist the token.
340
+ """
341
+ logger.info(f"User logged out: {current_user.username}")
342
+ return {"message": "Successfully logged out"}
343
+
344
+
345
+ @router.post("/test-login")
346
+ async def test_login():
347
+ """
348
+ Test endpoint to verify authentication system is working.
349
+ Returns sample login credentials.
350
+ """
351
+ return {
352
+ "message": "Authentication system is ready",
353
+ "test_credentials": [
354
+ {
355
+ "type": "Super Admin",
356
+ "email": "superadmin@cuatrobeauty.com",
357
+ "password": "SuperAdmin@123",
358
+ "description": "Full system access"
359
+ },
360
+ {
361
+ "type": "Company Admin",
362
+ "email": "admin@cuatrobeauty.com",
363
+ "password": "CompanyAdmin@123",
364
+ "description": "Company-wide management"
365
+ },
366
+ {
367
+ "type": "Manager",
368
+ "email": "manager@cuatrobeauty.com",
369
+ "password": "Manager@123",
370
+ "description": "Team management"
371
+ }
372
+ ]
373
+ }
374
+
375
+
376
+ @router.get("/access-roles")
377
+ async def get_access_roles(
378
+ user_service: SystemUserService = Depends(get_system_user_service)
379
+ ):
380
+ """
381
+ Get available access roles and their permissions structure.
382
+
383
+ Returns the complete role hierarchy with grouped permissions.
384
+ """
385
+ try:
386
+ # Get roles from database
387
+ roles = await user_service.get_all_roles()
388
+
389
+ return {
390
+ "message": "Access roles with grouped permissions structure",
391
+ "total_roles": len(roles),
392
+ "roles": [
393
+ {
394
+ "role_id": role.get("role_id"),
395
+ "role_name": role.get("role_name"),
396
+ "description": role.get("description"),
397
+ "permissions": role.get("permissions", {}),
398
+ "is_active": role.get("is_active", True)
399
+ }
400
+ for role in roles
401
+ ]
402
+ }
403
+ except Exception as e:
404
+ logger.error(f"Error fetching access roles: {e}")
405
+ return {
406
+ "message": "Error fetching access roles",
407
+ "error": str(e)
408
+ }
app/cache.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cache module for Auth microservice - Redis connection and caching utilities
3
+ """
4
+ import redis
5
+ from typing import Optional, Any
6
+ import json
7
+ import logging
8
+ from app.core.config import settings
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class CacheService:
14
+ """Redis cache service for authentication system"""
15
+
16
+ def __init__(self):
17
+ self._redis_client: Optional[redis.Redis] = None
18
+
19
+ def get_client(self) -> redis.Redis:
20
+ """Get Redis client instance"""
21
+ if self._redis_client is None:
22
+ self._redis_client = redis.Redis(
23
+ host=settings.REDIS_HOST,
24
+ port=settings.REDIS_PORT,
25
+ password=settings.REDIS_PASSWORD,
26
+ db=settings.REDIS_DB,
27
+ decode_responses=True
28
+ )
29
+ return self._redis_client
30
+
31
+ async def set(self, key: str, value: Any, ttl: int = 300) -> bool:
32
+ """Set a value in cache with TTL"""
33
+ try:
34
+ client = self.get_client()
35
+ serialized_value = json.dumps(value) if not isinstance(value, str) else value
36
+ return client.setex(key, ttl, serialized_value)
37
+ except Exception as e:
38
+ logger.error(f"Cache set error: {e}")
39
+ return False
40
+
41
+ async def get(self, key: str) -> Optional[Any]:
42
+ """Get a value from cache"""
43
+ try:
44
+ client = self.get_client()
45
+ value = client.get(key)
46
+ if value:
47
+ try:
48
+ return json.loads(value)
49
+ except json.JSONDecodeError:
50
+ return value
51
+ return None
52
+ except Exception as e:
53
+ logger.error(f"Cache get error: {e}")
54
+ return None
55
+
56
+ async def delete(self, key: str) -> bool:
57
+ """Delete a key from cache"""
58
+ try:
59
+ client = self.get_client()
60
+ return client.delete(key) > 0
61
+ except Exception as e:
62
+ logger.error(f"Cache delete error: {e}")
63
+ return False
64
+
65
+
66
+ # Global cache instance
67
+ cache_service = CacheService()
app/constants/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Constants module for Auth microservice
3
+ """
app/constants/collections.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MongoDB collection names for Auth microservice.
3
+ """
4
+
5
+ # Collection names
6
+ AUTH_SYSTEM_USERS_COLLECTION = "auth_system_users"
7
+ AUTH_ACCESS_ROLES_COLLECTION = "auth_access_roles"
8
+ AUTH_AUTH_LOGS_COLLECTION = "auth_auth_logs"
9
+ AUTH_OTP_COLLECTION = "auth_otp"
10
+ AUTH_PASSWORD_RESET_COLLECTION = "auth_password_reset"
11
+ AUTH_SESSIONS_COLLECTION = "auth_sessions"
app/core/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Core configuration module
3
+ """
app/core/config.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for Auth microservice.
3
+ Loads environment variables and provides application settings.
4
+ """
5
+ import os
6
+ from typing import Optional, List
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+
9
+
10
+ class Settings(BaseSettings):
11
+ """Application settings loaded from environment variables"""
12
+
13
+ # Application
14
+ APP_NAME: str = "Auth Microservice"
15
+ APP_VERSION: str = "1.0.0"
16
+ DEBUG: bool = False
17
+
18
+ # MongoDB Configuration
19
+ MONGODB_URI: str = os.getenv("MONGODB_URI", "mongodb://localhost:27017")
20
+ MONGODB_DB_NAME: str = os.getenv("MONGODB_DB_NAME", "auth_db")
21
+
22
+ # Redis Configuration
23
+ REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
24
+ REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
25
+ REDIS_PASSWORD: Optional[str] = os.getenv("REDIS_PASSWORD")
26
+ REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
27
+
28
+ # JWT Configuration
29
+ SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
30
+ ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
31
+ TOKEN_EXPIRATION_HOURS: int = int(os.getenv("TOKEN_EXPIRATION_HOURS", "8"))
32
+
33
+ # OTP Configuration
34
+ OTP_TTL_SECONDS: int = int(os.getenv("OTP_TTL_SECONDS", "600"))
35
+ OTP_RATE_LIMIT_MAX: int = int(os.getenv("OTP_RATE_LIMIT_MAX", "10"))
36
+ OTP_RATE_LIMIT_WINDOW: int = int(os.getenv("OTP_RATE_LIMIT_WINDOW", "600"))
37
+
38
+ # Twilio Configuration
39
+ TWILIO_ACCOUNT_SID: Optional[str] = os.getenv("TWILIO_ACCOUNT_SID")
40
+ TWILIO_AUTH_TOKEN: Optional[str] = os.getenv("TWILIO_AUTH_TOKEN")
41
+ TWILIO_PHONE_NUMBER: Optional[str] = os.getenv("TWILIO_PHONE_NUMBER")
42
+
43
+ # SMTP Configuration
44
+ SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST")
45
+ SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587"))
46
+ SMTP_USERNAME: Optional[str] = os.getenv("SMTP_USERNAME")
47
+ SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD")
48
+ SMTP_FROM_EMAIL: Optional[str] = os.getenv("SMTP_FROM_EMAIL")
49
+ SMTP_USE_TLS: bool = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
50
+
51
+ # Logging
52
+ LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
53
+
54
+ # CORS
55
+ CORS_ORIGINS: List[str] = [
56
+ "http://localhost:3000",
57
+ "http://localhost:8000",
58
+ "http://localhost:8002",
59
+ ]
60
+
61
+ # Pydantic v2 config
62
+ model_config = SettingsConfigDict(
63
+ env_file=".env",
64
+ env_file_encoding="utf-8",
65
+ case_sensitive=True,
66
+ extra="allow", # allows extra environment variables without error
67
+ )
68
+
69
+
70
+ # Global settings instance
71
+ settings = Settings()
app/dependencies/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Dependencies module
3
+ """
app/dependencies/auth.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication dependencies for FastAPI.
3
+ """
4
+ from typing import Optional
5
+ from datetime import datetime
6
+ from fastapi import Depends, HTTPException, status
7
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
8
+ 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
+
15
+ def get_system_user_service() -> SystemUserService:
16
+ """
17
+ Dependency to get SystemUserService instance.
18
+
19
+ Returns:
20
+ SystemUserService: Service instance with database connection
21
+ """
22
+ # get_database() returns AsyncIOMotorDatabase directly, no await needed
23
+ db = get_database()
24
+ return SystemUserService(db)
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
+ """Get current authenticated user from JWT token."""
32
+
33
+ credentials_exception = HTTPException(
34
+ status_code=status.HTTP_401_UNAUTHORIZED,
35
+ detail="Could not validate credentials",
36
+ headers={"WWW-Authenticate": "Bearer"},
37
+ )
38
+
39
+ try:
40
+ # Verify token
41
+ payload = user_service.verify_token(credentials.credentials, "access")
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 Exception:
50
+ raise credentials_exception
51
+
52
+ # Get user from database
53
+ user = await user_service.get_user_by_id(user_id)
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 not active"
62
+ )
63
+
64
+ return user
65
+
66
+
67
+ async def get_current_active_user(
68
+ current_user: SystemUserModel = Depends(get_current_user)
69
+ ) -> SystemUserModel:
70
+ """Get current active user (alias for get_current_user for clarity)."""
71
+ return current_user
72
+
73
+
74
+ async def require_admin_role(
75
+ current_user: SystemUserModel = Depends(get_current_user)
76
+ ) -> SystemUserModel:
77
+ """Require admin or super_admin role."""
78
+ if current_user.role not in [UserRole.ADMIN, UserRole.SUPER_ADMIN]:
79
+ raise HTTPException(
80
+ status_code=status.HTTP_403_FORBIDDEN,
81
+ detail="Admin privileges required"
82
+ )
83
+ return current_user
84
+
85
+
86
+ async def require_super_admin_role(
87
+ current_user: SystemUserModel = Depends(get_current_user)
88
+ ) -> SystemUserModel:
89
+ """Require super_admin role."""
90
+ if current_user.role != UserRole.SUPER_ADMIN:
91
+ raise HTTPException(
92
+ status_code=status.HTTP_403_FORBIDDEN,
93
+ detail="Super admin privileges required"
94
+ )
95
+ return current_user
96
+
97
+
98
+ def require_permission(permission: str):
99
+ """Dependency factory to require specific permission."""
100
+ async def permission_checker(
101
+ current_user: SystemUserModel = Depends(get_current_user)
102
+ ) -> SystemUserModel:
103
+ if (permission not in current_user.permissions and
104
+ current_user.role not in [UserRole.ADMIN, UserRole.SUPER_ADMIN]):
105
+ raise HTTPException(
106
+ status_code=status.HTTP_403_FORBIDDEN,
107
+ detail=f"Permission '{permission}' required"
108
+ )
109
+ return current_user
110
+
111
+ return permission_checker
112
+
113
+
114
+ 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
+ """Get current user if token is provided, otherwise return None."""
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")
130
+ if user_id is None:
131
+ return None
132
+
133
+ # Get user from database
134
+ user = await user_service.get_user_by_id(user_id)
135
+ if user is None or user.status.value != "active":
136
+ return None
137
+
138
+ return user
139
+
140
+ except Exception:
141
+ return None
app/insightfy_utils-0.1.0-py3-none-any.whl ADDED
Binary file (32.2 kB). View file
 
app/main.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
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
11
+ from app.system_users.controllers.router import router as system_user_router
12
+ from app.auth.controllers.router import router as auth_router
13
+
14
+ logger = logging.getLogger(__name__)
15
+ logging.basicConfig(level=logging.INFO)
16
+
17
+
18
+ @asynccontextmanager
19
+ async def lifespan(app: FastAPI):
20
+ """Manage application lifespan events"""
21
+ # Startup
22
+ logger.info("Starting AUTH Microservice")
23
+ await connect_to_mongo()
24
+ logger.info("AUTH Microservice started successfully")
25
+
26
+ yield
27
+
28
+ # Shutdown
29
+ logger.info("Shutting down AUTH Microservice")
30
+ await close_mongo_connection()
31
+ logger.info("AUTH Microservice shut down successfully")
32
+
33
+
34
+ # Create FastAPI app
35
+ app = FastAPI(
36
+ title="AUTH Microservice",
37
+ description="Authentication & Authorization System - User Management, Login, JWT Tokens & Security",
38
+ version="1.0.0",
39
+ docs_url="/docs",
40
+ redoc_url="/redoc",
41
+ lifespan=lifespan
42
+ )
43
+
44
+ # CORS middleware
45
+ app.add_middleware(
46
+ CORSMiddleware,
47
+ allow_origins=settings.CORS_ORIGINS,
48
+ allow_credentials=True,
49
+ allow_methods=["*"],
50
+ allow_headers=["*"],
51
+ )
52
+
53
+
54
+ # Health check endpoint
55
+ @app.get("/health", tags=["health"])
56
+ async def health_check():
57
+ """Health check endpoint"""
58
+ return {
59
+ "status": "healthy",
60
+ "service": "auth-microservice",
61
+ "version": "1.0.0"
62
+ }
63
+
64
+
65
+ # Include routers
66
+ app.include_router(auth_router) # Authentication endpoints (login, logout, token refresh)
67
+ app.include_router(system_user_router) # User management endpoints (CRUD operations)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ import uvicorn
72
+ uvicorn.run(
73
+ "app.main:app",
74
+ host="0.0.0.0",
75
+ port=8002,
76
+ reload=True,
77
+ log_level="info"
78
+ )
app/nosql.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MongoDB connection and database instance.
3
+ Provides a singleton database connection for the application.
4
+ """
5
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
6
+ import logging
7
+ from app.core.config import settings
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class DatabaseConnection:
13
+ """Singleton class to manage MongoDB connection"""
14
+ _client: AsyncIOMotorClient = None
15
+ _db: AsyncIOMotorDatabase = None
16
+
17
+ @classmethod
18
+ def get_database(cls) -> AsyncIOMotorDatabase:
19
+ """
20
+ Get the database instance.
21
+
22
+ Returns:
23
+ MongoDB database instance
24
+
25
+ Raises:
26
+ RuntimeError if database is not connected
27
+ """
28
+ if cls._db is None:
29
+ raise RuntimeError("Database not connected. Call connect_to_mongo() first.")
30
+ return cls._db
31
+
32
+ @classmethod
33
+ async def connect(cls):
34
+ """
35
+ Establish connection to MongoDB.
36
+ Called during application startup.
37
+ """
38
+ try:
39
+ logger.info(f"Connecting to MongoDB: {settings.MONGODB_URI}")
40
+
41
+ cls._client = AsyncIOMotorClient(settings.MONGODB_URI)
42
+ cls._db = cls._client[settings.MONGODB_DB_NAME]
43
+
44
+ # Test the connection
45
+ await cls._client.admin.command('ping')
46
+
47
+ logger.info(f"Successfully connected to MongoDB database: {settings.MONGODB_DB_NAME}")
48
+ except Exception as e:
49
+ logger.error(f"Failed to connect to MongoDB: {e}")
50
+ raise
51
+
52
+ @classmethod
53
+ async def close(cls):
54
+ """
55
+ Close MongoDB connection.
56
+ Called during application shutdown.
57
+ """
58
+ if cls._client:
59
+ logger.info("Closing MongoDB connection")
60
+ cls._client.close()
61
+ logger.info("MongoDB connection closed")
62
+
63
+
64
+ # Public API
65
+ async def connect_to_mongo():
66
+ """Establish connection to MongoDB"""
67
+ await DatabaseConnection.connect()
68
+
69
+
70
+ async def close_mongo_connection():
71
+ """Close MongoDB connection"""
72
+ await DatabaseConnection.close()
73
+
74
+
75
+ def get_database() -> AsyncIOMotorDatabase:
76
+ """Get the database instance"""
77
+ return DatabaseConnection.get_database()
app/system_users/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ System Users module for authentication and user management
3
+ """
app/system_users/controllers/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ System Users controllers
3
+ """
app/system_users/controllers/router.py ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ System User router for authentication and user management endpoints.
3
+ """
4
+ from datetime import timedelta
5
+ from typing import List, Optional
6
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
7
+ from fastapi.security import HTTPAuthorizationCredentials
8
+ import logging
9
+
10
+ from app.system_users.services.service import SystemUserService, ACCESS_TOKEN_EXPIRE_MINUTES
11
+ from app.system_users.schemas.schema import (
12
+ LoginRequest,
13
+ LoginResponse,
14
+ CreateUserRequest,
15
+ UpdateUserRequest,
16
+ ChangePasswordRequest,
17
+ UserInfoResponse,
18
+ UserListResponse,
19
+ StandardResponse
20
+ )
21
+ from app.system_users.models.model import UserStatus, SystemUserModel
22
+ from app.dependencies.auth import (
23
+ get_system_user_service,
24
+ get_current_user,
25
+ require_admin_role,
26
+ require_super_admin_role
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ router = APIRouter(
32
+ prefix="/auth",
33
+ tags=["Authentication & User Management"]
34
+ )
35
+
36
+
37
+ @router.post("/login", response_model=LoginResponse)
38
+ async def login(
39
+ request: Request,
40
+ login_data: LoginRequest,
41
+ user_service: SystemUserService = Depends(get_system_user_service)
42
+ ):
43
+ """
44
+ Authenticate user and return access token.
45
+ """
46
+ try:
47
+ # Get client IP and user agent
48
+ client_ip = request.client.host if request.client else None
49
+ user_agent = request.headers.get("User-Agent")
50
+
51
+ # Authenticate user
52
+ user, message = await user_service.authenticate_user(
53
+ email_or_phone=login_data.email_or_phone,
54
+ password=login_data.password,
55
+ ip_address=client_ip,
56
+ user_agent=user_agent
57
+ )
58
+
59
+ if not user:
60
+ raise HTTPException(
61
+ status_code=status.HTTP_401_UNAUTHORIZED,
62
+ detail=message,
63
+ headers={"WWW-Authenticate": "Bearer"},
64
+ )
65
+
66
+ # Create access token
67
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
68
+ if login_data.remember_me:
69
+ access_token_expires = timedelta(hours=24) # Longer expiry for remember me
70
+
71
+ access_token = user_service.create_access_token(
72
+ data={"sub": user.user_id, "username": user.username, "role": user.role.value},
73
+ expires_delta=access_token_expires
74
+ )
75
+
76
+ # Convert user to response model
77
+ user_info = user_service.convert_to_user_info_response(user)
78
+
79
+ logger.info(f"User logged in successfully: {user.username}")
80
+
81
+ return LoginResponse(
82
+ access_token=access_token,
83
+ token_type="bearer",
84
+ expires_in=int(access_token_expires.total_seconds()),
85
+ user_info=user_info
86
+ )
87
+
88
+ except HTTPException:
89
+ raise
90
+ except Exception as e:
91
+ logger.error(f"Login error: {e}")
92
+ raise HTTPException(
93
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
94
+ detail="Login failed"
95
+ )
96
+
97
+
98
+ @router.get("/me", response_model=UserInfoResponse)
99
+ async def get_current_user_info(
100
+ current_user: SystemUserModel = Depends(get_current_user),
101
+ user_service: SystemUserService = Depends(get_system_user_service)
102
+ ):
103
+ """
104
+ Get current user information.
105
+ """
106
+ return user_service.convert_to_user_info_response(current_user)
107
+
108
+
109
+ @router.post("/users", response_model=UserInfoResponse)
110
+ async def create_user(
111
+ user_data: CreateUserRequest,
112
+ current_user: SystemUserModel = Depends(require_admin_role),
113
+ user_service: SystemUserService = Depends(get_system_user_service)
114
+ ):
115
+ """
116
+ Create a new user account. Requires admin privileges.
117
+ """
118
+ try:
119
+ new_user = await user_service.create_user(user_data, current_user.user_id)
120
+ return user_service.convert_to_user_info_response(new_user)
121
+
122
+ except HTTPException:
123
+ raise
124
+ except Exception as e:
125
+ logger.error(f"Error creating user: {e}")
126
+ raise HTTPException(
127
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
128
+ detail="Failed to create user"
129
+ )
130
+
131
+
132
+ @router.get("/users", response_model=UserListResponse)
133
+ async def list_users(
134
+ page: int = 1,
135
+ page_size: int = 20,
136
+ status_filter: Optional[UserStatus] = None,
137
+ current_user: SystemUserModel = Depends(require_admin_role),
138
+ user_service: SystemUserService = Depends(get_system_user_service)
139
+ ):
140
+ """
141
+ List users with pagination. Requires admin privileges.
142
+ """
143
+ try:
144
+ if page_size > 100:
145
+ page_size = 100 # Limit maximum page size
146
+
147
+ users, total_count = await user_service.list_users(page, page_size, status_filter)
148
+
149
+ user_responses = [
150
+ user_service.convert_to_user_info_response(user) for user in users
151
+ ]
152
+
153
+ return UserListResponse(
154
+ users=user_responses,
155
+ total_count=total_count,
156
+ page=page,
157
+ page_size=page_size
158
+ )
159
+
160
+ except Exception as e:
161
+ logger.error(f"Error listing users: {e}")
162
+ raise HTTPException(
163
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
164
+ detail="Failed to retrieve users"
165
+ )
166
+
167
+
168
+ @router.get("/users/{user_id}", response_model=UserInfoResponse)
169
+ async def get_user_by_id(
170
+ user_id: str,
171
+ current_user: SystemUserModel = Depends(require_admin_role),
172
+ user_service: SystemUserService = Depends(get_system_user_service)
173
+ ):
174
+ """
175
+ Get user by ID. Requires admin privileges.
176
+ """
177
+ user = await user_service.get_user_by_id(user_id)
178
+ if not user:
179
+ raise HTTPException(
180
+ status_code=status.HTTP_404_NOT_FOUND,
181
+ detail="User not found"
182
+ )
183
+
184
+ return user_service.convert_to_user_info_response(user)
185
+
186
+
187
+ @router.put("/users/{user_id}", response_model=UserInfoResponse)
188
+ async def update_user(
189
+ user_id: str,
190
+ update_data: UpdateUserRequest,
191
+ current_user: SystemUserModel = Depends(require_admin_role),
192
+ user_service: SystemUserService = Depends(get_system_user_service)
193
+ ):
194
+ """
195
+ Update user information. Requires admin privileges.
196
+ """
197
+ try:
198
+ updated_user = await user_service.update_user(user_id, update_data, current_user.user_id)
199
+ if not updated_user:
200
+ raise HTTPException(
201
+ status_code=status.HTTP_404_NOT_FOUND,
202
+ detail="User not found"
203
+ )
204
+
205
+ return user_service.convert_to_user_info_response(updated_user)
206
+
207
+ except HTTPException:
208
+ raise
209
+ except Exception as e:
210
+ logger.error(f"Error updating user {user_id}: {e}")
211
+ raise HTTPException(
212
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
213
+ detail="Failed to update user"
214
+ )
215
+
216
+
217
+ @router.put("/change-password", response_model=StandardResponse)
218
+ async def change_password(
219
+ password_data: ChangePasswordRequest,
220
+ current_user: SystemUserModel = Depends(get_current_user),
221
+ user_service: SystemUserService = Depends(get_system_user_service)
222
+ ):
223
+ """
224
+ Change current user's password.
225
+ """
226
+ try:
227
+ success = await user_service.change_password(
228
+ user_id=current_user.user_id,
229
+ current_password=password_data.current_password,
230
+ new_password=password_data.new_password
231
+ )
232
+
233
+ if not success:
234
+ raise HTTPException(
235
+ status_code=status.HTTP_400_BAD_REQUEST,
236
+ detail="Current password is incorrect"
237
+ )
238
+
239
+ return StandardResponse(
240
+ success=True,
241
+ message="Password changed successfully"
242
+ )
243
+
244
+ except HTTPException:
245
+ raise
246
+ except Exception as e:
247
+ logger.error(f"Error changing password for user {current_user.user_id}: {e}")
248
+ raise HTTPException(
249
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
250
+ detail="Failed to change password"
251
+ )
252
+
253
+
254
+ @router.delete("/users/{user_id}", response_model=StandardResponse)
255
+ async def deactivate_user(
256
+ user_id: str,
257
+ current_user: SystemUserModel = Depends(require_admin_role),
258
+ user_service: SystemUserService = Depends(get_system_user_service)
259
+ ):
260
+ """
261
+ Deactivate user account. Requires admin privileges.
262
+ """
263
+ try:
264
+ # Prevent self-deactivation
265
+ if user_id == current_user.user_id:
266
+ raise HTTPException(
267
+ status_code=status.HTTP_400_BAD_REQUEST,
268
+ detail="Cannot deactivate your own account"
269
+ )
270
+
271
+ success = await user_service.deactivate_user(user_id, current_user.user_id)
272
+ if not success:
273
+ raise HTTPException(
274
+ status_code=status.HTTP_404_NOT_FOUND,
275
+ detail="User not found"
276
+ )
277
+
278
+ return StandardResponse(
279
+ success=True,
280
+ message="User deactivated successfully"
281
+ )
282
+
283
+ except HTTPException:
284
+ raise
285
+ except Exception as e:
286
+ logger.error(f"Error deactivating user {user_id}: {e}")
287
+ raise HTTPException(
288
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
289
+ detail="Failed to deactivate user"
290
+ )
291
+
292
+
293
+ @router.post("/logout", response_model=StandardResponse)
294
+ async def logout(
295
+ current_user: SystemUserModel = Depends(get_current_user)
296
+ ):
297
+ """
298
+ Logout current user.
299
+ Note: Since we're using stateless JWT tokens, actual logout would require
300
+ token blacklisting on the client side or implementing a token blacklist on server.
301
+ """
302
+ logger.info(f"User logged out: {current_user.username}")
303
+
304
+ return StandardResponse(
305
+ success=True,
306
+ message="Logged out successfully"
307
+ )
308
+
309
+
310
+ # Create default super admin endpoint (for initial setup)
311
+ @router.post("/setup/super-admin", response_model=UserInfoResponse)
312
+ async def create_super_admin(
313
+ user_data: CreateUserRequest,
314
+ user_service: SystemUserService = Depends(get_system_user_service)
315
+ ):
316
+ """
317
+ Create the first super admin user. Only works if no users exist in the system.
318
+ """
319
+ try:
320
+ # Check if any users exist
321
+ users, total_count = await user_service.list_users(page=1, page_size=1)
322
+ if total_count > 0:
323
+ raise HTTPException(
324
+ status_code=status.HTTP_403_FORBIDDEN,
325
+ detail="Super admin already exists or users are present in system"
326
+ )
327
+
328
+ # Force super admin role
329
+ user_data.role = "super_admin"
330
+
331
+ # Create super admin
332
+ super_admin = await user_service.create_user(user_data, "system")
333
+
334
+ logger.info(f"Super admin created: {super_admin.username}")
335
+
336
+ return user_service.convert_to_user_info_response(super_admin)
337
+
338
+ except HTTPException:
339
+ raise
340
+ except Exception as e:
341
+ logger.error(f"Error creating super admin: {e}")
342
+ raise HTTPException(
343
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
344
+ detail="Failed to create super admin"
345
+ )
app/system_users/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ System Users models
3
+ """
app/system_users/models/model.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ System User model for authentication and authorization.
3
+ Represents a system user with login credentials and permissions.
4
+ """
5
+ from datetime import datetime
6
+ from typing import Optional, Dict, Any, List
7
+ from pydantic import BaseModel, Field, EmailStr
8
+ from enum import Enum
9
+
10
+
11
+ class UserStatus(str, Enum):
12
+ """User account status options."""
13
+ ACTIVE = "active"
14
+ INACTIVE = "inactive"
15
+ SUSPENDED = "suspended"
16
+ LOCKED = "locked"
17
+ PENDING_ACTIVATION = "pending_activation"
18
+
19
+
20
+ class UserRole(str, Enum):
21
+ """System user roles."""
22
+ SUPER_ADMIN = "super_admin"
23
+ ADMIN = "admin"
24
+ MANAGER = "manager"
25
+ USER = "user"
26
+ READ_ONLY = "read_only"
27
+
28
+
29
+ class LoginAttemptModel(BaseModel):
30
+ """Login attempt tracking."""
31
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
32
+ ip_address: Optional[str] = Field(None, description="IP address of login attempt")
33
+ user_agent: Optional[str] = Field(None, description="User agent string")
34
+ success: bool = Field(..., description="Whether login was successful")
35
+ failure_reason: Optional[str] = Field(None, description="Reason for failure if unsuccessful")
36
+
37
+
38
+ class SecuritySettingsModel(BaseModel):
39
+ """User security settings."""
40
+ require_password_change: bool = Field(default=False, description="Force password change on next login")
41
+ password_expires_at: Optional[datetime] = Field(None, description="Password expiry date")
42
+ failed_login_attempts: int = Field(default=0, description="Count of consecutive failed login attempts")
43
+ last_failed_login: Optional[datetime] = Field(None, description="Timestamp of last failed login")
44
+ account_locked_until: Optional[datetime] = Field(None, description="Account lock expiry time")
45
+ last_password_change: Optional[datetime] = Field(None, description="Last password change timestamp")
46
+ login_attempts: List[LoginAttemptModel] = Field(default_factory=list, description="Recent login attempts (last 10)")
47
+
48
+
49
+ class SystemUserModel(BaseModel):
50
+ """
51
+ System User data model for authentication and authorization.
52
+ Represents the complete user document in MongoDB.
53
+ """
54
+ user_id: str = Field(..., description="Unique user identifier (UUID/ULID)")
55
+ username: str = Field(..., description="Unique username (lowercase alphanumeric)")
56
+ email: EmailStr = Field(..., description="User email address")
57
+
58
+ # Authentication
59
+ password_hash: str = Field(..., description="Bcrypt hashed password")
60
+
61
+ # Personal information
62
+ first_name: str = Field(..., description="User first name")
63
+ last_name: Optional[str] = Field(None, description="User last name")
64
+ phone: Optional[str] = Field(None, description="User phone number (E.164 format)")
65
+
66
+ # Authorization
67
+ role: UserRole = Field(default=UserRole.USER, description="Primary user role")
68
+ permissions: Dict[str, List[str]] = Field(default_factory=dict, description="Grouped permissions by module")
69
+
70
+ # Status and security
71
+ status: UserStatus = Field(default=UserStatus.PENDING_ACTIVATION, description="Account status")
72
+ security_settings: SecuritySettingsModel = Field(
73
+ default_factory=SecuritySettingsModel,
74
+ description="Security and login settings"
75
+ )
76
+
77
+ # Session management
78
+ last_login_at: Optional[datetime] = Field(None, description="Last successful login timestamp")
79
+ last_login_ip: Optional[str] = Field(None, description="IP address of last login")
80
+ current_session_token: Optional[str] = Field(None, description="Current JWT token hash for session management")
81
+
82
+ # Profile information
83
+ profile_picture_url: Optional[str] = Field(None, description="URL to profile picture")
84
+ timezone: str = Field(default="UTC", description="User timezone")
85
+ language: str = Field(default="en", description="Preferred language code")
86
+
87
+ # Audit fields
88
+ created_by: str = Field(..., description="User ID who created this user account")
89
+ created_at: datetime = Field(default_factory=datetime.utcnow, description="Account creation timestamp")
90
+ updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
91
+ updated_by: Optional[str] = Field(None, description="User ID who last updated this record")
92
+
93
+ # Additional data
94
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata")
95
+
96
+ class Config:
97
+ json_schema_extra = {
98
+ "example": {
99
+ "user_id": "usr_01HZQX5K3N2P8R6T4V9W",
100
+ "username": "john.doe",
101
+ "email": "john.doe@company.com",
102
+ "password_hash": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LeVMstdMT6jmDQrji",
103
+ "first_name": "John",
104
+ "last_name": "Doe",
105
+ "phone": "+919876543210",
106
+ "role": "admin",
107
+ "permissions": {
108
+ "customers": ["view", "create", "update"],
109
+ "orders": ["view", "create", "update"],
110
+ "settings": ["view", "update"]
111
+ },
112
+ "status": "active",
113
+ "security_settings": {
114
+ "require_password_change": False,
115
+ "failed_login_attempts": 0,
116
+ "login_attempts": []
117
+ },
118
+ "last_login_at": "2024-11-30T10:30:00Z",
119
+ "last_login_ip": "192.168.1.100",
120
+ "timezone": "Asia/Kolkata",
121
+ "language": "en",
122
+ "created_by": "system",
123
+ "created_at": "2024-01-15T08:00:00Z"
124
+ }
125
+ }
app/system_users/schemas/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ System Users schemas
3
+ """
app/system_users/schemas/schema.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ System User schemas for request/response models.
3
+ """
4
+ from datetime import datetime
5
+ from typing import Optional, List, Dict
6
+ from pydantic import BaseModel, Field, EmailStr, validator
7
+ from app.system_users.models.model import UserStatus, UserRole
8
+
9
+
10
+ class LoginRequest(BaseModel):
11
+ """Login request schema."""
12
+ email_or_phone: str = Field(..., description="Email address or phone number", min_length=3, max_length=100)
13
+ password: str = Field(..., description="User password", min_length=6, max_length=100)
14
+ remember_me: bool = Field(default=False, description="Keep session active for longer period")
15
+
16
+
17
+ class LoginResponse(BaseModel):
18
+ """Login response schema."""
19
+ access_token: str = Field(..., description="JWT access token")
20
+ token_type: str = Field(default="bearer", description="Token type")
21
+ expires_in: int = Field(..., description="Token expiry time in seconds")
22
+ user_info: "UserInfoResponse" = Field(..., description="Basic user information")
23
+
24
+
25
+ class UserInfoResponse(BaseModel):
26
+ """User information response schema."""
27
+ user_id: str = Field(..., description="Unique user identifier")
28
+ username: str = Field(..., description="Username")
29
+ email: str = Field(..., description="Email address")
30
+ first_name: str = Field(..., description="First name")
31
+ last_name: Optional[str] = Field(None, description="Last name")
32
+ role: UserRole = Field(..., description="User role")
33
+ permissions: Dict[str, List[str]] = Field(default_factory=dict, description="User permissions")
34
+ status: UserStatus = Field(..., description="Account status")
35
+ last_login_at: Optional[datetime] = Field(None, description="Last login timestamp")
36
+ profile_picture_url: Optional[str] = Field(None, description="Profile picture URL")
37
+
38
+
39
+ class CreateUserRequest(BaseModel):
40
+ """Create user request schema."""
41
+ username: str = Field(..., description="Unique username", min_length=3, max_length=30)
42
+ email: EmailStr = Field(..., description="Email address")
43
+ password: str = Field(..., description="Password", min_length=8, max_length=100)
44
+ first_name: str = Field(..., description="First name", min_length=1, max_length=50)
45
+ last_name: Optional[str] = Field(None, description="Last name", max_length=50)
46
+ phone: Optional[str] = Field(None, description="Phone number")
47
+ role: UserRole = Field(default=UserRole.USER, description="User role")
48
+ permissions: Dict[str, List[str]] = Field(default_factory=dict, description="Additional permissions")
49
+
50
+ @validator('username')
51
+ def validate_username(cls, v):
52
+ if not v.replace('_', '').replace('.', '').isalnum():
53
+ raise ValueError('Username can only contain alphanumeric characters, underscores, and periods')
54
+ return v.lower()
55
+
56
+ @validator('password')
57
+ def validate_password(cls, v):
58
+ if len(v) < 8:
59
+ raise ValueError('Password must be at least 8 characters long')
60
+ if not any(c.isupper() for c in v):
61
+ raise ValueError('Password must contain at least one uppercase letter')
62
+ if not any(c.islower() for c in v):
63
+ raise ValueError('Password must contain at least one lowercase letter')
64
+ if not any(c.isdigit() for c in v):
65
+ raise ValueError('Password must contain at least one digit')
66
+ return v
67
+
68
+
69
+ class UpdateUserRequest(BaseModel):
70
+ """Update user request schema."""
71
+ first_name: Optional[str] = Field(None, description="First name", min_length=1, max_length=50)
72
+ last_name: Optional[str] = Field(None, description="Last name", max_length=50)
73
+ phone: Optional[str] = Field(None, description="Phone number")
74
+ role: Optional[UserRole] = Field(None, description="User role")
75
+ permissions: Optional[Dict[str, List[str]]] = Field(None, description="User permissions")
76
+ status: Optional[UserStatus] = Field(None, description="Account status")
77
+ timezone: Optional[str] = Field(None, description="User timezone")
78
+ language: Optional[str] = Field(None, description="Preferred language")
79
+
80
+
81
+ class ChangePasswordRequest(BaseModel):
82
+ """Change password request schema."""
83
+ current_password: str = Field(..., description="Current password")
84
+ new_password: str = Field(..., description="New password", min_length=8, max_length=100)
85
+
86
+ @validator('new_password')
87
+ def validate_new_password(cls, v):
88
+ if len(v) < 8:
89
+ raise ValueError('Password must be at least 8 characters long')
90
+ if not any(c.isupper() for c in v):
91
+ raise ValueError('Password must contain at least one uppercase letter')
92
+ if not any(c.islower() for c in v):
93
+ raise ValueError('Password must contain at least one lowercase letter')
94
+ if not any(c.isdigit() for c in v):
95
+ raise ValueError('Password must contain at least one digit')
96
+ return v
97
+
98
+
99
+ class ResetPasswordRequest(BaseModel):
100
+ """Reset password request schema."""
101
+ email: EmailStr = Field(..., description="Email address")
102
+
103
+
104
+ class UserListResponse(BaseModel):
105
+ """User list response schema."""
106
+ users: List[UserInfoResponse] = Field(..., description="List of users")
107
+ total_count: int = Field(..., description="Total number of users")
108
+ page: int = Field(..., description="Current page number")
109
+ page_size: int = Field(..., description="Number of items per page")
110
+
111
+
112
+ class TokenRefreshRequest(BaseModel):
113
+ """Token refresh request schema."""
114
+ refresh_token: str = Field(..., description="Refresh token")
115
+
116
+
117
+ class LogoutRequest(BaseModel):
118
+ """Logout request schema."""
119
+ logout_from_all_devices: bool = Field(default=False, description="Logout from all devices")
120
+
121
+
122
+ # Response models
123
+ class StandardResponse(BaseModel):
124
+ """Standard API response."""
125
+ success: bool = Field(..., description="Operation success status")
126
+ message: str = Field(..., description="Response message")
127
+ data: Optional[dict] = Field(None, description="Response data")
128
+
129
+
130
+ # Update forward references
131
+ LoginResponse.model_rebuild()
app/system_users/services/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ System Users services
3
+ """
app/system_users/services/service.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ System User service for authentication and user management.
3
+ """
4
+ import secrets
5
+ from datetime import datetime, timedelta
6
+ from typing import Optional, List, Dict, Any, Tuple
7
+ from motor.motor_asyncio import AsyncIOMotorDatabase
8
+ from passlib.context import CryptContext
9
+ from jose import JWTError, jwt
10
+ from fastapi import HTTPException, status
11
+ import logging
12
+
13
+ from app.system_users.models.model import (
14
+ SystemUserModel,
15
+ UserStatus,
16
+ UserRole,
17
+ LoginAttemptModel,
18
+ SecuritySettingsModel
19
+ )
20
+ from app.system_users.schemas.schema import (
21
+ CreateUserRequest,
22
+ UpdateUserRequest,
23
+ ChangePasswordRequest,
24
+ UserInfoResponse
25
+ )
26
+ from app.constants.collections import AUTH_SYSTEM_USERS_COLLECTION, AUTH_ACCESS_ROLES_COLLECTION
27
+ from app.core.config import settings
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Password hashing context
32
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
33
+
34
+ # JWT settings
35
+ ALGORITHM = "HS256"
36
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
37
+ REFRESH_TOKEN_EXPIRE_DAYS = 7
38
+ MAX_FAILED_LOGIN_ATTEMPTS = 5
39
+ ACCOUNT_LOCK_DURATION_MINUTES = 15
40
+
41
+
42
+ class SystemUserService:
43
+ """Service class for system user operations."""
44
+
45
+ def __init__(self, db: AsyncIOMotorDatabase):
46
+ self.db = db
47
+ self.collection = db[AUTH_SYSTEM_USERS_COLLECTION]
48
+
49
+ @staticmethod
50
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
51
+ """Verify a password against its hash."""
52
+ try:
53
+ return pwd_context.verify(plain_password, hashed_password)
54
+ except Exception as e:
55
+ logger.error(f"Error verifying password: {e}")
56
+ return False
57
+
58
+ @staticmethod
59
+ def get_password_hash(password: str) -> str:
60
+ """Generate password hash."""
61
+ return pwd_context.hash(password)
62
+
63
+ @staticmethod
64
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
65
+ """Create JWT access token."""
66
+ to_encode = data.copy()
67
+ if expires_delta:
68
+ expire = datetime.utcnow() + expires_delta
69
+ else:
70
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
71
+
72
+ to_encode.update({"exp": expire, "type": "access"})
73
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
74
+ return encoded_jwt
75
+
76
+ @staticmethod
77
+ def create_refresh_token(data: dict) -> str:
78
+ """Create JWT refresh token."""
79
+ to_encode = data.copy()
80
+ expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
81
+ to_encode.update({"exp": expire, "type": "refresh"})
82
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
83
+ return encoded_jwt
84
+
85
+ @staticmethod
86
+ def verify_token(token: str, token_type: str = "access") -> Optional[Dict[str, Any]]:
87
+ """Verify JWT token and return payload."""
88
+ try:
89
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
90
+ if payload.get("type") != token_type:
91
+ return None
92
+ return payload
93
+ except JWTError as e:
94
+ logger.warning(f"Token verification failed: {e}")
95
+ return None
96
+
97
+ async def get_user_by_id(self, user_id: str) -> Optional[SystemUserModel]:
98
+ """Get user by user_id."""
99
+ try:
100
+ user_doc = await self.collection.find_one({"user_id": user_id})
101
+ if user_doc:
102
+ return SystemUserModel(**user_doc)
103
+ return None
104
+ except Exception as e:
105
+ logger.error(f"Error getting user by ID {user_id}: {e}")
106
+ return None
107
+
108
+ async def get_user_by_username(self, username: str) -> Optional[SystemUserModel]:
109
+ """Get user by username."""
110
+ try:
111
+ user_doc = await self.collection.find_one({"username": username.lower()})
112
+ if user_doc:
113
+ return SystemUserModel(**user_doc)
114
+ return None
115
+ except Exception as e:
116
+ logger.error(f"Error getting user by username {username}: {e}")
117
+ return None
118
+
119
+ async def get_user_by_email(self, email: str) -> Optional[SystemUserModel]:
120
+ """Get user by email."""
121
+ try:
122
+ user_doc = await self.collection.find_one({"email": email.lower()})
123
+ if user_doc:
124
+ return SystemUserModel(**user_doc)
125
+ return None
126
+ except Exception as e:
127
+ logger.error(f"Error getting user by email {email}: {e}")
128
+ return None
129
+
130
+ async def get_user_by_phone(self, phone: str) -> Optional[SystemUserModel]:
131
+ """Get user by phone number."""
132
+ try:
133
+ user_doc = await self.collection.find_one({"phone": phone})
134
+ if user_doc:
135
+ return SystemUserModel(**user_doc)
136
+ return None
137
+ except Exception as e:
138
+ logger.error(f"Error getting user by phone {phone}: {e}")
139
+ return None
140
+
141
+ async def create_user(self, user_data: CreateUserRequest, created_by: str) -> SystemUserModel:
142
+ """Create a new user."""
143
+ try:
144
+ # Check if username or email already exists
145
+ existing_user = await self.get_user_by_username(user_data.username)
146
+ if existing_user:
147
+ raise HTTPException(
148
+ status_code=status.HTTP_400_BAD_REQUEST,
149
+ detail="Username already exists"
150
+ )
151
+
152
+ existing_email = await self.get_user_by_email(user_data.email)
153
+ if existing_email:
154
+ raise HTTPException(
155
+ status_code=status.HTTP_400_BAD_REQUEST,
156
+ detail="Email already exists"
157
+ )
158
+
159
+ # Generate user ID
160
+ user_id = f"usr_{secrets.token_urlsafe(16)}"
161
+
162
+ # Hash password
163
+ password_hash = self.get_password_hash(user_data.password)
164
+
165
+ # Create user model
166
+ user_model = SystemUserModel(
167
+ user_id=user_id,
168
+ username=user_data.username.lower(),
169
+ email=user_data.email.lower(),
170
+ password_hash=password_hash,
171
+ first_name=user_data.first_name,
172
+ last_name=user_data.last_name,
173
+ phone=user_data.phone,
174
+ role=user_data.role,
175
+ permissions=user_data.permissions,
176
+ status=UserStatus.ACTIVE, # Set as active by default
177
+ created_by=created_by,
178
+ created_at=datetime.utcnow()
179
+ )
180
+
181
+ # Insert to database
182
+ await self.collection.insert_one(user_model.model_dump())
183
+
184
+ logger.info(f"User created successfully: {user_id}")
185
+ return user_model
186
+
187
+ except HTTPException:
188
+ raise
189
+ except Exception as e:
190
+ logger.error(f"Error creating user: {e}")
191
+ raise HTTPException(
192
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
193
+ detail="Failed to create user"
194
+ )
195
+
196
+ async def authenticate_user(self, email_or_phone: str, password: str, ip_address: Optional[str] = None, user_agent: Optional[str] = None) -> Tuple[Optional[SystemUserModel], str]:
197
+ """Authenticate user with email/phone/username and password."""
198
+ try:
199
+ # Get user by email, phone, or username
200
+ user = await self.get_user_by_email(email_or_phone)
201
+ if not user:
202
+ user = await self.get_user_by_phone(email_or_phone)
203
+ if not user:
204
+ user = await self.get_user_by_username(email_or_phone)
205
+
206
+ # Record failed attempt if user not found
207
+ if not user:
208
+ logger.warning(f"Login attempt with non-existent email/phone/username: {email_or_phone}")
209
+ return None, "Invalid email, phone number, or username"
210
+
211
+ # Check if account is locked
212
+ if (user.security_settings.account_locked_until and
213
+ user.security_settings.account_locked_until > datetime.utcnow()):
214
+ return None, f"Account is locked until {user.security_settings.account_locked_until}"
215
+
216
+ # Check account status
217
+ if user.status not in [UserStatus.ACTIVE]:
218
+ return None, f"Account is {user.status.value}"
219
+
220
+ # Verify password
221
+ if not self.verify_password(password, user.password_hash):
222
+ await self._record_failed_login(user, ip_address, user_agent)
223
+ return None, "Invalid username or password"
224
+
225
+ # Password correct - reset failed attempts and record successful login
226
+ await self._record_successful_login(user, ip_address, user_agent)
227
+
228
+ return user, "Authentication successful"
229
+
230
+ except Exception as e:
231
+ logger.error(f"Error during authentication: {e}")
232
+ return None, "Authentication failed"
233
+
234
+ async def _record_failed_login(self, user: SystemUserModel, ip_address: Optional[str], user_agent: Optional[str]):
235
+ """Record failed login attempt and update security settings."""
236
+ try:
237
+ failed_attempts = user.security_settings.failed_login_attempts + 1
238
+ now = datetime.utcnow()
239
+
240
+ # Add login attempt to history
241
+ login_attempt = LoginAttemptModel(
242
+ timestamp=now,
243
+ ip_address=ip_address,
244
+ user_agent=user_agent,
245
+ success=False,
246
+ failure_reason="Invalid password"
247
+ )
248
+
249
+ # Keep only last 10 attempts
250
+ attempts_history = user.security_settings.login_attempts[-9:] + [login_attempt]
251
+
252
+ update_data = {
253
+ "security_settings.failed_login_attempts": failed_attempts,
254
+ "security_settings.last_failed_login": now,
255
+ "security_settings.login_attempts": [attempt.model_dump() for attempt in attempts_history],
256
+ "updated_at": now
257
+ }
258
+
259
+ # Lock account if too many failed attempts
260
+ if failed_attempts >= MAX_FAILED_LOGIN_ATTEMPTS:
261
+ lock_until = now + timedelta(minutes=ACCOUNT_LOCK_DURATION_MINUTES)
262
+ update_data["security_settings.account_locked_until"] = lock_until
263
+ logger.warning(f"Account locked due to failed login attempts: {user.user_id}")
264
+
265
+ await self.collection.update_one(
266
+ {"user_id": user.user_id},
267
+ {"$set": update_data}
268
+ )
269
+
270
+ except Exception as e:
271
+ logger.error(f"Error recording failed login: {e}")
272
+
273
+ async def _record_successful_login(self, user: SystemUserModel, ip_address: Optional[str], user_agent: Optional[str]):
274
+ """Record successful login and reset security counters."""
275
+ try:
276
+ now = datetime.utcnow()
277
+
278
+ # Add login attempt to history
279
+ login_attempt = LoginAttemptModel(
280
+ timestamp=now,
281
+ ip_address=ip_address,
282
+ user_agent=user_agent,
283
+ success=True
284
+ )
285
+
286
+ # Keep only last 10 attempts
287
+ attempts_history = user.security_settings.login_attempts[-9:] + [login_attempt]
288
+
289
+ await self.collection.update_one(
290
+ {"user_id": user.user_id},
291
+ {"$set": {
292
+ "last_login_at": now,
293
+ "last_login_ip": ip_address,
294
+ "security_settings.failed_login_attempts": 0,
295
+ "security_settings.last_failed_login": None,
296
+ "security_settings.account_locked_until": None,
297
+ "security_settings.login_attempts": [attempt.model_dump() for attempt in attempts_history],
298
+ "updated_at": now
299
+ }}
300
+ )
301
+
302
+ except Exception as e:
303
+ logger.error(f"Error recording successful login: {e}")
304
+
305
+ async def update_user(self, user_id: str, update_data: UpdateUserRequest, updated_by: str) -> Optional[SystemUserModel]:
306
+ """Update user information."""
307
+ try:
308
+ user = await self.get_user_by_id(user_id)
309
+ if not user:
310
+ return None
311
+
312
+ update_dict = {}
313
+ for field, value in update_data.dict(exclude_unset=True).items():
314
+ if value is not None:
315
+ update_dict[field] = value
316
+
317
+ if update_dict:
318
+ update_dict["updated_at"] = datetime.utcnow()
319
+ update_dict["updated_by"] = updated_by
320
+
321
+ await self.collection.update_one(
322
+ {"user_id": user_id},
323
+ {"$set": update_dict}
324
+ )
325
+
326
+ return await self.get_user_by_id(user_id)
327
+
328
+ except Exception as e:
329
+ logger.error(f"Error updating user {user_id}: {e}")
330
+ return None
331
+
332
+ async def change_password(self, user_id: str, current_password: str, new_password: str) -> bool:
333
+ """Change user password."""
334
+ try:
335
+ user = await self.get_user_by_id(user_id)
336
+ if not user:
337
+ return False
338
+
339
+ # Verify current password
340
+ if not self.verify_password(current_password, user.password_hash):
341
+ return False
342
+
343
+ # Hash new password
344
+ new_password_hash = self.get_password_hash(new_password)
345
+
346
+ # Update password
347
+ await self.collection.update_one(
348
+ {"user_id": user_id},
349
+ {"$set": {
350
+ "password_hash": new_password_hash,
351
+ "security_settings.last_password_change": datetime.utcnow(),
352
+ "security_settings.require_password_change": False,
353
+ "updated_at": datetime.utcnow()
354
+ }}
355
+ )
356
+
357
+ logger.info(f"Password changed for user: {user_id}")
358
+ return True
359
+
360
+ except Exception as e:
361
+ logger.error(f"Error changing password for user {user_id}: {e}")
362
+ return False
363
+
364
+ async def list_users(self, page: int = 1, page_size: int = 20, status_filter: Optional[UserStatus] = None) -> Tuple[List[SystemUserModel], int]:
365
+ """List users with pagination."""
366
+ try:
367
+ skip = (page - 1) * page_size
368
+
369
+ # Build query filter
370
+ query_filter = {}
371
+ if status_filter:
372
+ query_filter["status"] = status_filter.value
373
+
374
+ # Get total count
375
+ total_count = await self.collection.count_documents(query_filter)
376
+
377
+ # Get users - don't exclude password_hash to avoid validation errors
378
+ cursor = self.collection.find(query_filter).skip(skip).limit(page_size).sort("created_at", -1)
379
+ users = []
380
+ async for user_doc in cursor:
381
+ users.append(SystemUserModel(**user_doc))
382
+ return users, total_count
383
+
384
+ except Exception as e:
385
+ logger.error(f"Error listing users: {e}")
386
+ return [], 0
387
+
388
+ async def deactivate_user(self, user_id: str, deactivated_by: str) -> bool:
389
+ """Deactivate user account."""
390
+ try:
391
+ result = await self.collection.update_one(
392
+ {"user_id": user_id},
393
+ {"$set": {
394
+ "status": UserStatus.INACTIVE.value,
395
+ "updated_at": datetime.utcnow(),
396
+ "updated_by": deactivated_by
397
+ }}
398
+ )
399
+
400
+ if result.modified_count > 0:
401
+ logger.info(f"User deactivated: {user_id}")
402
+ return True
403
+ return False
404
+
405
+ except Exception as e:
406
+ logger.error(f"Error deactivating user {user_id}: {e}")
407
+ return False
408
+
409
+ def convert_to_user_info_response(self, user: SystemUserModel) -> UserInfoResponse:
410
+ """Convert SystemUserModel to UserInfoResponse."""
411
+ return UserInfoResponse(
412
+ user_id=user.user_id,
413
+ username=user.username,
414
+ email=user.email,
415
+ first_name=user.first_name,
416
+ last_name=user.last_name,
417
+ role=user.role,
418
+ permissions=user.permissions,
419
+ status=user.status,
420
+ last_login_at=user.last_login_at,
421
+ profile_picture_url=user.profile_picture_url
422
+ )
423
+
424
+ async def get_all_roles(self) -> List[dict]:
425
+ """Get all access roles from database."""
426
+ try:
427
+ cursor = self.db[AUTH_ACCESS_ROLES_COLLECTION].find({})
428
+ roles = await cursor.to_list(length=None)
429
+ return roles
430
+ except Exception as e:
431
+ logger.error(f"Error fetching access roles: {e}")
432
+ return []
app/utils/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Utilities module for Auth microservice
3
+ """
manage_db.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database management utilities for Auth microservice
3
+ """
4
+ import asyncio
5
+ import logging
6
+ from motor.motor_asyncio import AsyncIOMotorClient
7
+ from app.core.config import settings
8
+ from app.constants.collections import (
9
+ AUTH_SYSTEM_USERS_COLLECTION,
10
+ AUTH_ACCESS_ROLES_COLLECTION
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def create_indexes():
17
+ """Create database indexes for better performance"""
18
+ try:
19
+ client = AsyncIOMotorClient(settings.MONGODB_URI)
20
+ db = client[settings.MONGODB_DB_NAME]
21
+
22
+ # System Users indexes
23
+ users_collection = db[AUTH_SYSTEM_USERS_COLLECTION]
24
+
25
+ # Create indexes for users collection
26
+ await users_collection.create_index("user_id", unique=True)
27
+ await users_collection.create_index("username", unique=True)
28
+ await users_collection.create_index("email", unique=True)
29
+ await users_collection.create_index("phone")
30
+ await users_collection.create_index("status")
31
+ await users_collection.create_index("created_at")
32
+
33
+ # Access Roles indexes
34
+ roles_collection = db[AUTH_ACCESS_ROLES_COLLECTION]
35
+ await roles_collection.create_index("role_id", unique=True)
36
+ await roles_collection.create_index("role_name", unique=True)
37
+
38
+ logger.info("Database indexes created successfully")
39
+
40
+ except Exception as e:
41
+ logger.error(f"Error creating indexes: {e}")
42
+ finally:
43
+ client.close()
44
+
45
+
46
+ async def create_default_roles():
47
+ """Create default system roles if they don't exist"""
48
+ try:
49
+ client = AsyncIOMotorClient(settings.MONGODB_URI)
50
+ db = client[settings.MONGODB_DB_NAME]
51
+ roles_collection = db[AUTH_ACCESS_ROLES_COLLECTION]
52
+
53
+ default_roles = [
54
+ {
55
+ "role_id": "role_super_admin",
56
+ "role_name": "super_admin",
57
+ "description": "Super Administrator with full system access",
58
+ "permissions": {
59
+ "users": ["view", "create", "update", "delete"],
60
+ "roles": ["view", "create", "update", "delete"],
61
+ "settings": ["view", "update"],
62
+ "auth": ["view", "manage"],
63
+ "system": ["view", "manage"]
64
+ },
65
+ "is_active": True
66
+ },
67
+ {
68
+ "role_id": "role_admin",
69
+ "role_name": "admin",
70
+ "description": "Administrator with limited system access",
71
+ "permissions": {
72
+ "users": ["view", "create", "update"],
73
+ "roles": ["view"],
74
+ "settings": ["view", "update"],
75
+ "auth": ["view"]
76
+ },
77
+ "is_active": True
78
+ },
79
+ {
80
+ "role_id": "role_manager",
81
+ "role_name": "manager",
82
+ "description": "Manager with team management capabilities",
83
+ "permissions": {
84
+ "users": ["view", "update"],
85
+ "auth": ["view"]
86
+ },
87
+ "is_active": True
88
+ },
89
+ {
90
+ "role_id": "role_user",
91
+ "role_name": "user",
92
+ "description": "Standard user with basic access",
93
+ "permissions": {
94
+ "auth": ["view"]
95
+ },
96
+ "is_active": True
97
+ }
98
+ ]
99
+
100
+ for role in default_roles:
101
+ existing = await roles_collection.find_one({"role_name": role["role_name"]})
102
+ if not existing:
103
+ await roles_collection.insert_one(role)
104
+ logger.info(f"Created default role: {role['role_name']}")
105
+
106
+ logger.info("Default roles setup completed")
107
+
108
+ except Exception as e:
109
+ logger.error(f"Error creating default roles: {e}")
110
+ finally:
111
+ client.close()
112
+
113
+
114
+ async def init_database():
115
+ """Initialize database with indexes and default data"""
116
+ logger.info("Initializing database...")
117
+ await create_indexes()
118
+ await create_default_roles()
119
+ logger.info("Database initialization completed")
120
+
121
+
122
+ if __name__ == "__main__":
123
+ asyncio.run(init_database())
requirements.txt CHANGED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ python-multipart==0.0.6
4
+
5
+ motor==3.3.2
6
+ pymongo==4.6.0
7
+ email-validator==2.3.0
8
+ redis==5.0.1
9
+
10
+ python-jose[cryptography]==3.3.0
11
+ passlib[bcrypt]==1.7.4
12
+ bcrypt==4.1.3
13
+ pydantic>=2.12.5,<3.0.0
14
+ pydantic-settings>=2.0.0
15
+
16
+ pytest==7.4.3
17
+ pytest-asyncio==0.21.1
18
+ httpx==0.25.2
19
+ hypothesis==6.92.1
20
+
21
+ python-dotenv==1.0.0
22
+
23
+ twilio==8.10.3
24
+ aiosmtplib==3.0.1
25
+
26
+ python-json-logger==2.0.7
start_server.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Start Auth Microservice
4
+
5
+ echo "Starting Auth Microservice..."
6
+
7
+ # Check if virtual environment exists
8
+ if [ ! -d "venv" ]; then
9
+ echo "Creating virtual environment..."
10
+ python3 -m venv venv
11
+ fi
12
+
13
+ # Activate virtual environment
14
+ source venv/bin/activate
15
+
16
+ # Install dependencies
17
+ echo "Installing dependencies..."
18
+ pip install -r requirements.txt
19
+
20
+ # Run the application
21
+ echo "Starting FastAPI server..."
22
+ uvicorn app.main:app --host 0.0.0.0 --port 8002 --reload