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.
Files changed (75) hide show
  1. mdb_engine/README.md +144 -0
  2. mdb_engine/__init__.py +37 -0
  3. mdb_engine/auth/README.md +631 -0
  4. mdb_engine/auth/__init__.py +128 -0
  5. mdb_engine/auth/casbin_factory.py +199 -0
  6. mdb_engine/auth/casbin_models.py +46 -0
  7. mdb_engine/auth/config_defaults.py +71 -0
  8. mdb_engine/auth/config_helpers.py +213 -0
  9. mdb_engine/auth/cookie_utils.py +158 -0
  10. mdb_engine/auth/decorators.py +350 -0
  11. mdb_engine/auth/dependencies.py +747 -0
  12. mdb_engine/auth/helpers.py +64 -0
  13. mdb_engine/auth/integration.py +578 -0
  14. mdb_engine/auth/jwt.py +225 -0
  15. mdb_engine/auth/middleware.py +241 -0
  16. mdb_engine/auth/oso_factory.py +323 -0
  17. mdb_engine/auth/provider.py +570 -0
  18. mdb_engine/auth/restrictions.py +271 -0
  19. mdb_engine/auth/session_manager.py +477 -0
  20. mdb_engine/auth/token_lifecycle.py +213 -0
  21. mdb_engine/auth/token_store.py +289 -0
  22. mdb_engine/auth/users.py +1516 -0
  23. mdb_engine/auth/utils.py +614 -0
  24. mdb_engine/cli/__init__.py +13 -0
  25. mdb_engine/cli/commands/__init__.py +7 -0
  26. mdb_engine/cli/commands/generate.py +105 -0
  27. mdb_engine/cli/commands/migrate.py +83 -0
  28. mdb_engine/cli/commands/show.py +70 -0
  29. mdb_engine/cli/commands/validate.py +63 -0
  30. mdb_engine/cli/main.py +41 -0
  31. mdb_engine/cli/utils.py +92 -0
  32. mdb_engine/config.py +217 -0
  33. mdb_engine/constants.py +160 -0
  34. mdb_engine/core/README.md +542 -0
  35. mdb_engine/core/__init__.py +42 -0
  36. mdb_engine/core/app_registration.py +392 -0
  37. mdb_engine/core/connection.py +243 -0
  38. mdb_engine/core/engine.py +749 -0
  39. mdb_engine/core/index_management.py +162 -0
  40. mdb_engine/core/manifest.py +2793 -0
  41. mdb_engine/core/seeding.py +179 -0
  42. mdb_engine/core/service_initialization.py +355 -0
  43. mdb_engine/core/types.py +413 -0
  44. mdb_engine/database/README.md +522 -0
  45. mdb_engine/database/__init__.py +31 -0
  46. mdb_engine/database/abstraction.py +635 -0
  47. mdb_engine/database/connection.py +387 -0
  48. mdb_engine/database/scoped_wrapper.py +1721 -0
  49. mdb_engine/embeddings/README.md +184 -0
  50. mdb_engine/embeddings/__init__.py +62 -0
  51. mdb_engine/embeddings/dependencies.py +193 -0
  52. mdb_engine/embeddings/service.py +759 -0
  53. mdb_engine/exceptions.py +167 -0
  54. mdb_engine/indexes/README.md +651 -0
  55. mdb_engine/indexes/__init__.py +21 -0
  56. mdb_engine/indexes/helpers.py +145 -0
  57. mdb_engine/indexes/manager.py +895 -0
  58. mdb_engine/memory/README.md +451 -0
  59. mdb_engine/memory/__init__.py +30 -0
  60. mdb_engine/memory/service.py +1285 -0
  61. mdb_engine/observability/README.md +515 -0
  62. mdb_engine/observability/__init__.py +42 -0
  63. mdb_engine/observability/health.py +296 -0
  64. mdb_engine/observability/logging.py +161 -0
  65. mdb_engine/observability/metrics.py +297 -0
  66. mdb_engine/routing/README.md +462 -0
  67. mdb_engine/routing/__init__.py +73 -0
  68. mdb_engine/routing/websockets.py +813 -0
  69. mdb_engine/utils/__init__.py +7 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +213 -0
  71. mdb_engine-0.1.6.dist-info/RECORD +75 -0
  72. mdb_engine-0.1.6.dist-info/WHEEL +5 -0
  73. mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
  74. mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
  75. mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
@@ -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__ = []
@@ -0,0 +1,7 @@
1
+ """
2
+ CLI commands for MDB_ENGINE.
3
+
4
+ This module contains command implementations for the CLI tool.
5
+ """
6
+
7
+ __all__ = []