mdb-engine 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdb_engine/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
mdb_engine/auth/utils.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication Utility Functions
|
|
3
|
+
|
|
4
|
+
High-level utility functions for common authentication flows.
|
|
5
|
+
|
|
6
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
import bcrypt
|
|
17
|
+
from fastapi import Request, Response
|
|
18
|
+
from fastapi.responses import JSONResponse, RedirectResponse
|
|
19
|
+
|
|
20
|
+
from .cookie_utils import clear_auth_cookies, set_auth_cookies
|
|
21
|
+
from .dependencies import SECRET_KEY, get_session_manager, get_token_blacklist
|
|
22
|
+
from .jwt import generate_token_pair
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _detect_browser(user_agent: str) -> str:
|
|
28
|
+
"""Detect browser from user agent string."""
|
|
29
|
+
if not user_agent:
|
|
30
|
+
return "unknown"
|
|
31
|
+
|
|
32
|
+
ua_lower = user_agent.lower()
|
|
33
|
+
if "chrome" in ua_lower and "edg" not in ua_lower:
|
|
34
|
+
return "chrome"
|
|
35
|
+
if "firefox" in ua_lower:
|
|
36
|
+
return "firefox"
|
|
37
|
+
if "safari" in ua_lower and "chrome" not in ua_lower:
|
|
38
|
+
return "safari"
|
|
39
|
+
if "edg" in ua_lower:
|
|
40
|
+
return "edge"
|
|
41
|
+
if "opera" in ua_lower:
|
|
42
|
+
return "opera"
|
|
43
|
+
return "unknown"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _detect_os_and_device_type(user_agent: str) -> Tuple[str, str]:
|
|
47
|
+
"""Detect OS and device type from user agent string."""
|
|
48
|
+
if not user_agent:
|
|
49
|
+
return "unknown", "desktop"
|
|
50
|
+
|
|
51
|
+
ua_lower = user_agent.lower()
|
|
52
|
+
if "windows" in ua_lower:
|
|
53
|
+
return "windows", "desktop"
|
|
54
|
+
if "mac" in ua_lower or "darwin" in ua_lower:
|
|
55
|
+
return "macos", "desktop"
|
|
56
|
+
if "linux" in ua_lower:
|
|
57
|
+
return "linux", "desktop"
|
|
58
|
+
if "android" in ua_lower:
|
|
59
|
+
return "android", "mobile"
|
|
60
|
+
if "iphone" in ua_lower:
|
|
61
|
+
return "ios", "mobile"
|
|
62
|
+
if "ipad" in ua_lower:
|
|
63
|
+
return "ios", "tablet"
|
|
64
|
+
return "unknown", "desktop"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_device_info(request: Request) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Extract device information from request.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
request: FastAPI Request object
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dictionary with device_id, user_agent, browser, OS, IP, device_type
|
|
76
|
+
"""
|
|
77
|
+
user_agent = request.headers.get("user-agent", "")
|
|
78
|
+
ip_address = request.client.host if request.client else None
|
|
79
|
+
|
|
80
|
+
# Generate or get device ID from cookie
|
|
81
|
+
device_id = request.cookies.get("device_id")
|
|
82
|
+
if not device_id:
|
|
83
|
+
device_id = str(uuid.uuid4())
|
|
84
|
+
|
|
85
|
+
browser = _detect_browser(user_agent)
|
|
86
|
+
os, device_type = _detect_os_and_device_type(user_agent)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"device_id": device_id,
|
|
90
|
+
"user_agent": user_agent,
|
|
91
|
+
"browser": browser,
|
|
92
|
+
"os": os,
|
|
93
|
+
"ip_address": ip_address,
|
|
94
|
+
"device_type": device_type,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate_password_strength(
|
|
99
|
+
password: str,
|
|
100
|
+
min_length: Optional[int] = None,
|
|
101
|
+
require_uppercase: Optional[bool] = None,
|
|
102
|
+
require_lowercase: Optional[bool] = None,
|
|
103
|
+
require_numbers: Optional[bool] = None,
|
|
104
|
+
require_special: Optional[bool] = None,
|
|
105
|
+
config: Optional[Dict[str, Any]] = None,
|
|
106
|
+
) -> Tuple[bool, List[str]]:
|
|
107
|
+
"""
|
|
108
|
+
Validate password strength with configurable rules.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
password: Password to validate
|
|
112
|
+
min_length: Minimum password length (default: from config or 8)
|
|
113
|
+
require_uppercase: Require uppercase letters (default: from config or True)
|
|
114
|
+
require_lowercase: Require lowercase letters (default: from config or True)
|
|
115
|
+
require_numbers: Require numbers (default: from config or True)
|
|
116
|
+
require_special: Require special characters (default: from config or False)
|
|
117
|
+
config: Optional password_policy config dict from manifest
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Tuple of (is_valid, list_of_errors)
|
|
121
|
+
"""
|
|
122
|
+
errors = []
|
|
123
|
+
|
|
124
|
+
if not password:
|
|
125
|
+
return False, ["Password is required"]
|
|
126
|
+
|
|
127
|
+
if config:
|
|
128
|
+
min_length = (
|
|
129
|
+
min_length if min_length is not None else config.get("min_length", 8)
|
|
130
|
+
)
|
|
131
|
+
require_uppercase = (
|
|
132
|
+
require_uppercase
|
|
133
|
+
if require_uppercase is not None
|
|
134
|
+
else config.get("require_uppercase", True)
|
|
135
|
+
)
|
|
136
|
+
require_lowercase = (
|
|
137
|
+
require_lowercase
|
|
138
|
+
if require_lowercase is not None
|
|
139
|
+
else config.get("require_lowercase", True)
|
|
140
|
+
)
|
|
141
|
+
require_numbers = (
|
|
142
|
+
require_numbers
|
|
143
|
+
if require_numbers is not None
|
|
144
|
+
else config.get("require_numbers", True)
|
|
145
|
+
)
|
|
146
|
+
require_special = (
|
|
147
|
+
require_special
|
|
148
|
+
if require_special is not None
|
|
149
|
+
else config.get("require_special", False)
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
min_length = min_length if min_length is not None else 8
|
|
153
|
+
require_uppercase = require_uppercase if require_uppercase is not None else True
|
|
154
|
+
require_lowercase = require_lowercase if require_lowercase is not None else True
|
|
155
|
+
require_numbers = require_numbers if require_numbers is not None else True
|
|
156
|
+
require_special = require_special if require_special is not None else False
|
|
157
|
+
|
|
158
|
+
if len(password) < min_length:
|
|
159
|
+
errors.append(f"Password must be at least {min_length} characters long")
|
|
160
|
+
|
|
161
|
+
if require_uppercase and not re.search(r"[A-Z]", password):
|
|
162
|
+
errors.append("Password must contain at least one uppercase letter")
|
|
163
|
+
|
|
164
|
+
if require_lowercase and not re.search(r"[a-z]", password):
|
|
165
|
+
errors.append("Password must contain at least one lowercase letter")
|
|
166
|
+
|
|
167
|
+
if require_numbers and not re.search(r"\d", password):
|
|
168
|
+
errors.append("Password must contain at least one number")
|
|
169
|
+
|
|
170
|
+
if require_special and not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
171
|
+
errors.append("Password must contain at least one special character")
|
|
172
|
+
|
|
173
|
+
return len(errors) == 0, errors
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def generate_session_fingerprint(request: Request, device_id: str) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Generate a session fingerprint from request characteristics.
|
|
179
|
+
|
|
180
|
+
Fingerprint is a hash of user-agent, IP address, device ID, and accept-language.
|
|
181
|
+
Used to detect session hijacking and unauthorized access.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
request: FastAPI Request object
|
|
185
|
+
device_id: Device identifier
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
SHA256 hash of fingerprint components as hex string
|
|
189
|
+
"""
|
|
190
|
+
components = [
|
|
191
|
+
request.headers.get("user-agent", ""),
|
|
192
|
+
request.client.host if request.client else "",
|
|
193
|
+
device_id,
|
|
194
|
+
request.headers.get("accept-language", ""),
|
|
195
|
+
]
|
|
196
|
+
fingerprint_string = "|".join(components)
|
|
197
|
+
return hashlib.sha256(fingerprint_string.encode()).hexdigest()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def login_user(
|
|
201
|
+
request: Request,
|
|
202
|
+
email: str,
|
|
203
|
+
password: str,
|
|
204
|
+
db,
|
|
205
|
+
config: Optional[Dict[str, Any]] = None,
|
|
206
|
+
remember_me: bool = False,
|
|
207
|
+
redirect_url: Optional[str] = None,
|
|
208
|
+
) -> Dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Handle user login with automatic token generation and cookie setting.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
request: FastAPI Request object
|
|
214
|
+
email: User email
|
|
215
|
+
password: User password
|
|
216
|
+
db: Database instance (top-level or app-specific)
|
|
217
|
+
config: Optional token_management config from manifest
|
|
218
|
+
remember_me: If True, extends token TTL (default: False)
|
|
219
|
+
redirect_url: Optional redirect URL after login (default: "/dashboard")
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Dictionary with:
|
|
223
|
+
- success: bool
|
|
224
|
+
- user: user dict if successful
|
|
225
|
+
- response: Response object with cookies set (if successful)
|
|
226
|
+
- error: error message (if failed)
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
# Validate email format
|
|
230
|
+
if not email or "@" not in email:
|
|
231
|
+
return {"success": False, "error": "Invalid email format"}
|
|
232
|
+
|
|
233
|
+
# Find user by email
|
|
234
|
+
user = await db.users.find_one({"email": email})
|
|
235
|
+
|
|
236
|
+
if not user:
|
|
237
|
+
return {"success": False, "error": "Invalid email or password"}
|
|
238
|
+
|
|
239
|
+
# Verify password
|
|
240
|
+
password_hash = user.get("password_hash") or user.get("password")
|
|
241
|
+
if not password_hash:
|
|
242
|
+
return {"success": False, "error": "Invalid email or password"}
|
|
243
|
+
|
|
244
|
+
# Check password (bcrypt only - plain text support removed for security)
|
|
245
|
+
password_valid = False
|
|
246
|
+
if isinstance(password_hash, bytes) or (
|
|
247
|
+
isinstance(password_hash, str) and password_hash.startswith("$2b$")
|
|
248
|
+
):
|
|
249
|
+
# Bcrypt hash
|
|
250
|
+
if isinstance(password_hash, str):
|
|
251
|
+
password_hash = password_hash.encode("utf-8")
|
|
252
|
+
if isinstance(password, str):
|
|
253
|
+
password_bytes = password.encode("utf-8")
|
|
254
|
+
else:
|
|
255
|
+
password_bytes = password
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
password_valid = bcrypt.checkpw(password_bytes, password_hash)
|
|
259
|
+
except (ValueError, TypeError, AttributeError) as e:
|
|
260
|
+
logger.debug(f"Bcrypt check failed: {e}")
|
|
261
|
+
password_valid = False
|
|
262
|
+
else:
|
|
263
|
+
# Password is not bcrypt hashed - reject for security
|
|
264
|
+
logger.warning(
|
|
265
|
+
f"User {email} has non-bcrypt password hash - password verification rejected"
|
|
266
|
+
)
|
|
267
|
+
password_valid = False
|
|
268
|
+
|
|
269
|
+
if not password_valid:
|
|
270
|
+
return {"success": False, "error": "Invalid email or password"}
|
|
271
|
+
|
|
272
|
+
# Get device info
|
|
273
|
+
device_info = get_device_info(request)
|
|
274
|
+
|
|
275
|
+
# Prepare user data for token
|
|
276
|
+
user_data = {
|
|
277
|
+
"user_id": str(user["_id"]),
|
|
278
|
+
"email": user["email"],
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Add role if present
|
|
282
|
+
if "role" in user:
|
|
283
|
+
user_data["role"] = user["role"]
|
|
284
|
+
|
|
285
|
+
# Get token TTLs from config
|
|
286
|
+
access_token_ttl = None
|
|
287
|
+
refresh_token_ttl = None
|
|
288
|
+
if config:
|
|
289
|
+
access_token_ttl = config.get("access_token_ttl")
|
|
290
|
+
refresh_token_ttl = config.get("refresh_token_ttl")
|
|
291
|
+
if remember_me:
|
|
292
|
+
# Extend refresh token TTL for remember me
|
|
293
|
+
refresh_token_ttl = refresh_token_ttl * 2 if refresh_token_ttl else None
|
|
294
|
+
|
|
295
|
+
# Generate token pair
|
|
296
|
+
access_token, refresh_token, token_metadata = generate_token_pair(
|
|
297
|
+
user_data,
|
|
298
|
+
str(SECRET_KEY),
|
|
299
|
+
device_info=device_info,
|
|
300
|
+
access_token_ttl=access_token_ttl,
|
|
301
|
+
refresh_token_ttl=refresh_token_ttl,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Create session if session manager available
|
|
305
|
+
session_mgr = await get_session_manager(request)
|
|
306
|
+
if session_mgr:
|
|
307
|
+
await session_mgr.create_session(
|
|
308
|
+
user_id=user_data["email"],
|
|
309
|
+
device_id=device_info["device_id"],
|
|
310
|
+
refresh_jti=token_metadata.get("refresh_jti"),
|
|
311
|
+
device_info=device_info,
|
|
312
|
+
ip_address=device_info.get("ip_address"),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Create response
|
|
316
|
+
if redirect_url:
|
|
317
|
+
response = RedirectResponse(url=redirect_url, status_code=302)
|
|
318
|
+
else:
|
|
319
|
+
response = JSONResponse(
|
|
320
|
+
{
|
|
321
|
+
"success": True,
|
|
322
|
+
"user": {"email": user["email"], "user_id": str(user["_id"])},
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Set cookies
|
|
327
|
+
set_auth_cookies(
|
|
328
|
+
response,
|
|
329
|
+
access_token,
|
|
330
|
+
refresh_token,
|
|
331
|
+
request=request,
|
|
332
|
+
config=config,
|
|
333
|
+
access_token_ttl=access_token_ttl,
|
|
334
|
+
refresh_token_ttl=refresh_token_ttl,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Set device_id cookie
|
|
338
|
+
response.set_cookie(
|
|
339
|
+
key="device_id",
|
|
340
|
+
value=device_info["device_id"],
|
|
341
|
+
max_age=31536000, # 1 year
|
|
342
|
+
httponly=False, # Allow JS access for device tracking
|
|
343
|
+
secure=request.url.scheme == "https" if request else False,
|
|
344
|
+
samesite="lax",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
"success": True,
|
|
349
|
+
"user": user,
|
|
350
|
+
"response": response,
|
|
351
|
+
"token_metadata": token_metadata,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
except (
|
|
355
|
+
ValueError,
|
|
356
|
+
TypeError,
|
|
357
|
+
AttributeError,
|
|
358
|
+
KeyError,
|
|
359
|
+
RuntimeError,
|
|
360
|
+
ConnectionError,
|
|
361
|
+
) as e:
|
|
362
|
+
logger.error(f"Error in login_user: {e}", exc_info=True)
|
|
363
|
+
return {"success": False, "error": "Login failed. Please try again."}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _validate_email_format(email: str) -> bool:
|
|
367
|
+
"""Validate basic email format."""
|
|
368
|
+
return bool(email and "@" in email and "." in email)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _get_password_policy_from_config(
|
|
372
|
+
request: Request, config: Optional[Dict[str, Any]]
|
|
373
|
+
) -> Optional[Dict[str, Any]]:
|
|
374
|
+
"""Get password policy from config or request."""
|
|
375
|
+
if config:
|
|
376
|
+
security = config.get("security", {})
|
|
377
|
+
return security.get("password_policy")
|
|
378
|
+
if hasattr(request, "app"):
|
|
379
|
+
from .config_helpers import get_password_policy
|
|
380
|
+
|
|
381
|
+
return get_password_policy(request)
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
async def _create_user_document(
|
|
386
|
+
email: str, password: str, extra_data: Optional[Dict[str, Any]]
|
|
387
|
+
) -> Dict[str, Any]:
|
|
388
|
+
"""Create user document with hashed password."""
|
|
389
|
+
password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
|
390
|
+
user_doc = {
|
|
391
|
+
"email": email,
|
|
392
|
+
"password_hash": password_hash,
|
|
393
|
+
"role": "user",
|
|
394
|
+
"date_created": datetime.utcnow(),
|
|
395
|
+
}
|
|
396
|
+
if extra_data:
|
|
397
|
+
user_doc.update(extra_data)
|
|
398
|
+
return user_doc
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _create_registration_response(
|
|
402
|
+
user_doc: Dict[str, Any], redirect_url: Optional[str]
|
|
403
|
+
) -> Response:
|
|
404
|
+
"""Create response for registration."""
|
|
405
|
+
if redirect_url:
|
|
406
|
+
return RedirectResponse(url=redirect_url, status_code=302)
|
|
407
|
+
return JSONResponse(
|
|
408
|
+
{
|
|
409
|
+
"success": True,
|
|
410
|
+
"user": {
|
|
411
|
+
"email": user_doc["email"],
|
|
412
|
+
"user_id": str(user_doc["_id"]),
|
|
413
|
+
},
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
async def register_user(
|
|
419
|
+
request: Request,
|
|
420
|
+
email: str,
|
|
421
|
+
password: str,
|
|
422
|
+
db,
|
|
423
|
+
config: Optional[Dict[str, Any]] = None,
|
|
424
|
+
extra_data: Optional[Dict[str, Any]] = None,
|
|
425
|
+
redirect_url: Optional[str] = None,
|
|
426
|
+
) -> Dict[str, Any]:
|
|
427
|
+
"""
|
|
428
|
+
Handle user registration with automatic token generation.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
request: FastAPI Request object
|
|
432
|
+
email: User email
|
|
433
|
+
password: User password
|
|
434
|
+
db: Database instance
|
|
435
|
+
config: Optional token_management config from manifest
|
|
436
|
+
extra_data: Optional extra user data to store
|
|
437
|
+
redirect_url: Optional redirect URL after registration
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Dictionary with success, user, response, or error
|
|
441
|
+
"""
|
|
442
|
+
try:
|
|
443
|
+
if not _validate_email_format(email):
|
|
444
|
+
return {"success": False, "error": "Invalid email format"}
|
|
445
|
+
|
|
446
|
+
password_policy = _get_password_policy_from_config(request, config)
|
|
447
|
+
is_valid, errors = validate_password_strength(password, config=password_policy)
|
|
448
|
+
if not is_valid:
|
|
449
|
+
return {"success": False, "error": "; ".join(errors)}
|
|
450
|
+
|
|
451
|
+
existing = await db.users.find_one({"email": email})
|
|
452
|
+
if existing:
|
|
453
|
+
return {"success": False, "error": "User with this email already exists"}
|
|
454
|
+
|
|
455
|
+
user_doc = await _create_user_document(email, password, extra_data)
|
|
456
|
+
result = await db.users.insert_one(user_doc)
|
|
457
|
+
user_doc["_id"] = result.inserted_id
|
|
458
|
+
|
|
459
|
+
device_info = get_device_info(request)
|
|
460
|
+
user_data = {
|
|
461
|
+
"user_id": str(user_doc["_id"]),
|
|
462
|
+
"email": user_doc["email"],
|
|
463
|
+
"role": user_doc.get("role", "user"),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
access_token, refresh_token, token_metadata = generate_token_pair(
|
|
467
|
+
user_data, str(SECRET_KEY), device_info=device_info
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
session_mgr = await get_session_manager(request)
|
|
471
|
+
if session_mgr:
|
|
472
|
+
await session_mgr.create_session(
|
|
473
|
+
user_id=user_data["email"],
|
|
474
|
+
device_id=device_info["device_id"],
|
|
475
|
+
refresh_jti=token_metadata.get("refresh_jti"),
|
|
476
|
+
device_info=device_info,
|
|
477
|
+
ip_address=device_info.get("ip_address"),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
response = _create_registration_response(user_doc, redirect_url)
|
|
481
|
+
set_auth_cookies(
|
|
482
|
+
response, access_token, refresh_token, request=request, config=config
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
response.set_cookie(
|
|
486
|
+
key="device_id",
|
|
487
|
+
value=device_info["device_id"],
|
|
488
|
+
max_age=31536000,
|
|
489
|
+
httponly=False,
|
|
490
|
+
secure=request.url.scheme == "https" if request else False,
|
|
491
|
+
samesite="lax",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
"success": True,
|
|
496
|
+
"user": user_doc,
|
|
497
|
+
"response": response,
|
|
498
|
+
"token_metadata": token_metadata,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
except (
|
|
502
|
+
ValueError,
|
|
503
|
+
TypeError,
|
|
504
|
+
AttributeError,
|
|
505
|
+
KeyError,
|
|
506
|
+
RuntimeError,
|
|
507
|
+
ConnectionError,
|
|
508
|
+
) as e:
|
|
509
|
+
logger.error(f"Error in register_user: {e}", exc_info=True)
|
|
510
|
+
return {"success": False, "error": "Registration failed. Please try again."}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
async def _get_user_id_from_request(
|
|
514
|
+
request: Request, user_id: Optional[str]
|
|
515
|
+
) -> Optional[str]:
|
|
516
|
+
"""Extract user_id from request if not provided."""
|
|
517
|
+
if user_id:
|
|
518
|
+
return user_id
|
|
519
|
+
|
|
520
|
+
from .dependencies import get_current_user_from_request
|
|
521
|
+
|
|
522
|
+
user = await get_current_user_from_request(request)
|
|
523
|
+
if user:
|
|
524
|
+
return user.get("email") or user.get("user_id")
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
async def _revoke_token_from_cookie(
|
|
529
|
+
request: Request,
|
|
530
|
+
cookie_name: str,
|
|
531
|
+
blacklist: Any,
|
|
532
|
+
user_id: str,
|
|
533
|
+
reason: str = "logout",
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Revoke a token from a cookie if present."""
|
|
536
|
+
token = request.cookies.get(cookie_name)
|
|
537
|
+
if not token:
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
from .jwt import extract_token_metadata
|
|
541
|
+
|
|
542
|
+
metadata = extract_token_metadata(token, str(SECRET_KEY))
|
|
543
|
+
if metadata:
|
|
544
|
+
jti = metadata.get("jti")
|
|
545
|
+
if jti:
|
|
546
|
+
await blacklist.revoke_token(jti, user_id=user_id, reason=reason)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
async def _revoke_all_tokens(request: Request, user_id: str) -> None:
|
|
550
|
+
"""Revoke all tokens (access and refresh) for a user."""
|
|
551
|
+
blacklist = await get_token_blacklist(request)
|
|
552
|
+
if not blacklist:
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
await _revoke_token_from_cookie(request, "token", blacklist, user_id)
|
|
556
|
+
await _revoke_token_from_cookie(request, "refresh_token", blacklist, user_id)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
async def _revoke_session(request: Request) -> None:
|
|
560
|
+
"""Revoke session using refresh token."""
|
|
561
|
+
session_mgr = await get_session_manager(request)
|
|
562
|
+
if not session_mgr:
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
refresh_token = request.cookies.get("refresh_token")
|
|
566
|
+
if not refresh_token:
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
from .jwt import extract_token_metadata
|
|
570
|
+
|
|
571
|
+
metadata = extract_token_metadata(refresh_token, str(SECRET_KEY))
|
|
572
|
+
if metadata:
|
|
573
|
+
refresh_jti = metadata.get("jti")
|
|
574
|
+
if refresh_jti:
|
|
575
|
+
await session_mgr.revoke_session_by_refresh_token(refresh_jti)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
async def logout_user(
|
|
579
|
+
request: Request, response: Response, user_id: Optional[str] = None
|
|
580
|
+
) -> Response:
|
|
581
|
+
"""
|
|
582
|
+
Handle user logout with token revocation and cookie clearing.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
request: FastAPI Request object
|
|
586
|
+
response: Response object to modify
|
|
587
|
+
user_id: Optional user ID (extracted from token if not provided)
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Response with cleared cookies
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
user_id = await _get_user_id_from_request(request, user_id)
|
|
594
|
+
|
|
595
|
+
if user_id:
|
|
596
|
+
await _revoke_all_tokens(request, user_id)
|
|
597
|
+
|
|
598
|
+
await _revoke_session(request)
|
|
599
|
+
clear_auth_cookies(response, request)
|
|
600
|
+
|
|
601
|
+
return response
|
|
602
|
+
|
|
603
|
+
except (
|
|
604
|
+
ValueError,
|
|
605
|
+
TypeError,
|
|
606
|
+
AttributeError,
|
|
607
|
+
KeyError,
|
|
608
|
+
RuntimeError,
|
|
609
|
+
ConnectionError,
|
|
610
|
+
) as e:
|
|
611
|
+
logger.error(f"Error in logout_user: {e}", exc_info=True)
|
|
612
|
+
# Still clear cookies even if revocation fails
|
|
613
|
+
clear_auth_cookies(response, request)
|
|
614
|
+
return response
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI tool for MDB_ENGINE manifest management.
|
|
3
|
+
|
|
4
|
+
This module provides command-line tools for:
|
|
5
|
+
- Validating manifests
|
|
6
|
+
- Migrating manifests between schema versions
|
|
7
|
+
- Generating template manifests
|
|
8
|
+
- Displaying manifest information
|
|
9
|
+
|
|
10
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__all__ = []
|