mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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/__init__.py +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
mdb_engine/auth/utils.py
CHANGED
|
@@ -11,7 +11,7 @@ import logging
|
|
|
11
11
|
import re
|
|
12
12
|
import uuid
|
|
13
13
|
from datetime import datetime
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
16
|
import bcrypt
|
|
17
17
|
from fastapi import Request, Response
|
|
@@ -43,7 +43,7 @@ def _detect_browser(user_agent: str) -> str:
|
|
|
43
43
|
return "unknown"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def _detect_os_and_device_type(user_agent: str) ->
|
|
46
|
+
def _detect_os_and_device_type(user_agent: str) -> tuple[str, str]:
|
|
47
47
|
"""Detect OS and device type from user agent string."""
|
|
48
48
|
if not user_agent:
|
|
49
49
|
return "unknown", "desktop"
|
|
@@ -64,7 +64,7 @@ def _detect_os_and_device_type(user_agent: str) -> Tuple[str, str]:
|
|
|
64
64
|
return "unknown", "desktop"
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def get_device_info(request: Request) ->
|
|
67
|
+
def get_device_info(request: Request) -> dict[str, Any]:
|
|
68
68
|
"""
|
|
69
69
|
Extract device information from request.
|
|
70
70
|
|
|
@@ -95,15 +95,156 @@ def get_device_info(request: Request) -> Dict[str, Any]:
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def calculate_password_entropy(password: str) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Calculate the entropy of a password in bits.
|
|
101
|
+
|
|
102
|
+
Entropy measures password randomness/unpredictability.
|
|
103
|
+
Higher entropy = more secure password.
|
|
104
|
+
|
|
105
|
+
Character set sizes:
|
|
106
|
+
- Lowercase: 26
|
|
107
|
+
- Uppercase: 26
|
|
108
|
+
- Digits: 10
|
|
109
|
+
- Special: ~32
|
|
110
|
+
|
|
111
|
+
Formula: entropy = length * log2(character_set_size)
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
password: Password to calculate entropy for
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Entropy in bits (float)
|
|
118
|
+
"""
|
|
119
|
+
import math
|
|
120
|
+
|
|
121
|
+
if not password:
|
|
122
|
+
return 0.0
|
|
123
|
+
|
|
124
|
+
# Determine character set size based on what's used
|
|
125
|
+
char_set_size = 0
|
|
126
|
+
|
|
127
|
+
if re.search(r"[a-z]", password):
|
|
128
|
+
char_set_size += 26
|
|
129
|
+
if re.search(r"[A-Z]", password):
|
|
130
|
+
char_set_size += 26
|
|
131
|
+
if re.search(r"\d", password):
|
|
132
|
+
char_set_size += 10
|
|
133
|
+
if re.search(r'[!@#$%^&*(),.?":{}|<>\[\]\\;\'`~_+=\-/]', password):
|
|
134
|
+
char_set_size += 32
|
|
135
|
+
if re.search(r"\s", password):
|
|
136
|
+
char_set_size += 1
|
|
137
|
+
|
|
138
|
+
if char_set_size == 0:
|
|
139
|
+
# Fallback for Unicode or other characters
|
|
140
|
+
char_set_size = 94 # Printable ASCII assumption
|
|
141
|
+
|
|
142
|
+
# Entropy = length * log2(char_set_size)
|
|
143
|
+
entropy = len(password) * math.log2(char_set_size)
|
|
144
|
+
|
|
145
|
+
return round(entropy, 2)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_common_password(password: str) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Check if password is in the common passwords list.
|
|
151
|
+
|
|
152
|
+
Uses a bundled list of the top 10,000 most common passwords.
|
|
153
|
+
Falls back gracefully if the file is not available.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
password: Password to check
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
True if password is common, False otherwise
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
import os
|
|
163
|
+
|
|
164
|
+
# Get the path to the common passwords file
|
|
165
|
+
resources_dir = os.path.join(os.path.dirname(__file__), "resources")
|
|
166
|
+
common_passwords_path = os.path.join(resources_dir, "common_passwords.txt")
|
|
167
|
+
|
|
168
|
+
if not os.path.exists(common_passwords_path):
|
|
169
|
+
logger.debug("Common passwords file not found, skipping check")
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# Read and check (case-insensitive)
|
|
173
|
+
password_lower = password.lower()
|
|
174
|
+
with open(common_passwords_path, encoding="utf-8") as f:
|
|
175
|
+
for line in f:
|
|
176
|
+
if line.strip().lower() == password_lower:
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
return False
|
|
180
|
+
except OSError as e:
|
|
181
|
+
logger.warning(f"Error checking common passwords: {e}")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def check_password_breach(password: str) -> bool:
|
|
186
|
+
"""
|
|
187
|
+
Check if password has been exposed in known data breaches.
|
|
188
|
+
|
|
189
|
+
Uses the HaveIBeenPwned API with k-anonymity (only sends first 5 chars of hash).
|
|
190
|
+
This is privacy-preserving - the full password hash is never sent.
|
|
191
|
+
|
|
192
|
+
Requires network access. Returns False (not breached) if check fails.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
password: Password to check
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if password was found in breaches, False otherwise
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
import hashlib
|
|
202
|
+
|
|
203
|
+
import httpx
|
|
204
|
+
|
|
205
|
+
# Hash the password
|
|
206
|
+
sha1_hash = hashlib.sha1(password.encode()).hexdigest().upper()
|
|
207
|
+
prefix = sha1_hash[:5]
|
|
208
|
+
suffix = sha1_hash[5:]
|
|
209
|
+
|
|
210
|
+
# Query HIBP API (k-anonymity: only send first 5 chars)
|
|
211
|
+
url = f"https://api.pwnedpasswords.com/range/{prefix}"
|
|
212
|
+
|
|
213
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
214
|
+
response = await client.get(url)
|
|
215
|
+
response.raise_for_status()
|
|
216
|
+
|
|
217
|
+
# Check if our suffix is in the response
|
|
218
|
+
for line in response.text.splitlines():
|
|
219
|
+
hash_suffix, count = line.split(":")
|
|
220
|
+
if hash_suffix == suffix:
|
|
221
|
+
logger.debug(f"Password found in {count} breaches")
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
except ImportError:
|
|
227
|
+
logger.debug("httpx not available, skipping breach check")
|
|
228
|
+
return False
|
|
229
|
+
except httpx.HTTPError as e:
|
|
230
|
+
logger.warning(f"Error checking password breaches: {e}")
|
|
231
|
+
return False
|
|
232
|
+
except (TimeoutError, ConnectionError, OSError) as e:
|
|
233
|
+
logger.warning(f"Error checking password breaches: {e}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
98
237
|
def validate_password_strength(
|
|
99
238
|
password: str,
|
|
100
|
-
min_length:
|
|
101
|
-
require_uppercase:
|
|
102
|
-
require_lowercase:
|
|
103
|
-
require_numbers:
|
|
104
|
-
require_special:
|
|
105
|
-
|
|
106
|
-
|
|
239
|
+
min_length: int | None = None,
|
|
240
|
+
require_uppercase: bool | None = None,
|
|
241
|
+
require_lowercase: bool | None = None,
|
|
242
|
+
require_numbers: bool | None = None,
|
|
243
|
+
require_special: bool | None = None,
|
|
244
|
+
min_entropy_bits: int | None = None,
|
|
245
|
+
check_common_passwords: bool | None = None,
|
|
246
|
+
config: dict[str, Any] | None = None,
|
|
247
|
+
) -> tuple[bool, list[str]]:
|
|
107
248
|
"""
|
|
108
249
|
Validate password strength with configurable rules.
|
|
109
250
|
|
|
@@ -114,6 +255,8 @@ def validate_password_strength(
|
|
|
114
255
|
require_lowercase: Require lowercase letters (default: from config or True)
|
|
115
256
|
require_numbers: Require numbers (default: from config or True)
|
|
116
257
|
require_special: Require special characters (default: from config or False)
|
|
258
|
+
min_entropy_bits: Minimum entropy in bits (default: from config or 0)
|
|
259
|
+
check_common_passwords: Check against common passwords (default: from config or False)
|
|
117
260
|
config: Optional password_policy config dict from manifest
|
|
118
261
|
|
|
119
262
|
Returns:
|
|
@@ -125,9 +268,7 @@ def validate_password_strength(
|
|
|
125
268
|
return False, ["Password is required"]
|
|
126
269
|
|
|
127
270
|
if config:
|
|
128
|
-
min_length = (
|
|
129
|
-
min_length if min_length is not None else config.get("min_length", 8)
|
|
130
|
-
)
|
|
271
|
+
min_length = min_length if min_length is not None else config.get("min_length", 8)
|
|
131
272
|
require_uppercase = (
|
|
132
273
|
require_uppercase
|
|
133
274
|
if require_uppercase is not None
|
|
@@ -139,14 +280,18 @@ def validate_password_strength(
|
|
|
139
280
|
else config.get("require_lowercase", True)
|
|
140
281
|
)
|
|
141
282
|
require_numbers = (
|
|
142
|
-
require_numbers
|
|
143
|
-
if require_numbers is not None
|
|
144
|
-
else config.get("require_numbers", True)
|
|
283
|
+
require_numbers if require_numbers is not None else config.get("require_numbers", True)
|
|
145
284
|
)
|
|
146
285
|
require_special = (
|
|
147
|
-
require_special
|
|
148
|
-
|
|
149
|
-
|
|
286
|
+
require_special if require_special is not None else config.get("require_special", False)
|
|
287
|
+
)
|
|
288
|
+
min_entropy_bits = (
|
|
289
|
+
min_entropy_bits if min_entropy_bits is not None else config.get("min_entropy_bits", 0)
|
|
290
|
+
)
|
|
291
|
+
check_common_passwords = (
|
|
292
|
+
check_common_passwords
|
|
293
|
+
if check_common_passwords is not None
|
|
294
|
+
else config.get("check_common_passwords", False)
|
|
150
295
|
)
|
|
151
296
|
else:
|
|
152
297
|
min_length = min_length if min_length is not None else 8
|
|
@@ -154,6 +299,10 @@ def validate_password_strength(
|
|
|
154
299
|
require_lowercase = require_lowercase if require_lowercase is not None else True
|
|
155
300
|
require_numbers = require_numbers if require_numbers is not None else True
|
|
156
301
|
require_special = require_special if require_special is not None else False
|
|
302
|
+
min_entropy_bits = min_entropy_bits if min_entropy_bits is not None else 0
|
|
303
|
+
check_common_passwords = (
|
|
304
|
+
check_common_passwords if check_common_passwords is not None else False
|
|
305
|
+
)
|
|
157
306
|
|
|
158
307
|
if len(password) < min_length:
|
|
159
308
|
errors.append(f"Password must be at least {min_length} characters long")
|
|
@@ -170,9 +319,62 @@ def validate_password_strength(
|
|
|
170
319
|
if require_special and not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
171
320
|
errors.append("Password must contain at least one special character")
|
|
172
321
|
|
|
322
|
+
# Entropy check
|
|
323
|
+
if min_entropy_bits and min_entropy_bits > 0:
|
|
324
|
+
entropy = calculate_password_entropy(password)
|
|
325
|
+
if entropy < min_entropy_bits:
|
|
326
|
+
errors.append(
|
|
327
|
+
f"Password is too weak (entropy: {entropy:.0f} bits, "
|
|
328
|
+
f"required: {min_entropy_bits} bits)"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Common password check
|
|
332
|
+
if check_common_passwords:
|
|
333
|
+
if is_common_password(password):
|
|
334
|
+
errors.append("Password is too common - please choose a unique password")
|
|
335
|
+
|
|
173
336
|
return len(errors) == 0, errors
|
|
174
337
|
|
|
175
338
|
|
|
339
|
+
async def validate_password_strength_async(
|
|
340
|
+
password: str,
|
|
341
|
+
config: dict[str, Any] | None = None,
|
|
342
|
+
check_breaches: bool | None = None,
|
|
343
|
+
) -> tuple[bool, list[str]]:
|
|
344
|
+
"""
|
|
345
|
+
Async version of validate_password_strength with breach checking.
|
|
346
|
+
|
|
347
|
+
This performs all synchronous checks, plus an optional async breach
|
|
348
|
+
check against HaveIBeenPwned.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
password: Password to validate
|
|
352
|
+
config: Optional password_policy config dict from manifest
|
|
353
|
+
check_breaches: Check against HIBP breach database (default: from config or False)
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Tuple of (is_valid, list_of_errors)
|
|
357
|
+
"""
|
|
358
|
+
# First, run sync validation
|
|
359
|
+
is_valid, errors = validate_password_strength(password, config=config)
|
|
360
|
+
|
|
361
|
+
# Determine if we should check breaches
|
|
362
|
+
if check_breaches is None and config:
|
|
363
|
+
check_breaches = config.get("check_breaches", False)
|
|
364
|
+
elif check_breaches is None:
|
|
365
|
+
check_breaches = False
|
|
366
|
+
|
|
367
|
+
# Async breach check
|
|
368
|
+
if check_breaches:
|
|
369
|
+
if await check_password_breach(password):
|
|
370
|
+
errors.append(
|
|
371
|
+
"Password has been exposed in a data breach - " "please choose a different password"
|
|
372
|
+
)
|
|
373
|
+
is_valid = False
|
|
374
|
+
|
|
375
|
+
return is_valid, errors
|
|
376
|
+
|
|
377
|
+
|
|
176
378
|
def generate_session_fingerprint(request: Request, device_id: str) -> str:
|
|
177
379
|
"""
|
|
178
380
|
Generate a session fingerprint from request characteristics.
|
|
@@ -202,10 +404,10 @@ async def login_user(
|
|
|
202
404
|
email: str,
|
|
203
405
|
password: str,
|
|
204
406
|
db,
|
|
205
|
-
config:
|
|
407
|
+
config: dict[str, Any] | None = None,
|
|
206
408
|
remember_me: bool = False,
|
|
207
|
-
redirect_url:
|
|
208
|
-
) ->
|
|
409
|
+
redirect_url: str | None = None,
|
|
410
|
+
) -> dict[str, Any]:
|
|
209
411
|
"""
|
|
210
412
|
Handle user login with automatic token generation and cookie setting.
|
|
211
413
|
|
|
@@ -369,8 +571,8 @@ def _validate_email_format(email: str) -> bool:
|
|
|
369
571
|
|
|
370
572
|
|
|
371
573
|
def _get_password_policy_from_config(
|
|
372
|
-
request: Request, config:
|
|
373
|
-
) ->
|
|
574
|
+
request: Request, config: dict[str, Any] | None
|
|
575
|
+
) -> dict[str, Any] | None:
|
|
374
576
|
"""Get password policy from config or request."""
|
|
375
577
|
if config:
|
|
376
578
|
security = config.get("security", {})
|
|
@@ -383,8 +585,8 @@ def _get_password_policy_from_config(
|
|
|
383
585
|
|
|
384
586
|
|
|
385
587
|
async def _create_user_document(
|
|
386
|
-
email: str, password: str, extra_data:
|
|
387
|
-
) ->
|
|
588
|
+
email: str, password: str, extra_data: dict[str, Any] | None
|
|
589
|
+
) -> dict[str, Any]:
|
|
388
590
|
"""Create user document with hashed password."""
|
|
389
591
|
password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
|
390
592
|
user_doc = {
|
|
@@ -398,9 +600,7 @@ async def _create_user_document(
|
|
|
398
600
|
return user_doc
|
|
399
601
|
|
|
400
602
|
|
|
401
|
-
def _create_registration_response(
|
|
402
|
-
user_doc: Dict[str, Any], redirect_url: Optional[str]
|
|
403
|
-
) -> Response:
|
|
603
|
+
def _create_registration_response(user_doc: dict[str, Any], redirect_url: str | None) -> Response:
|
|
404
604
|
"""Create response for registration."""
|
|
405
605
|
if redirect_url:
|
|
406
606
|
return RedirectResponse(url=redirect_url, status_code=302)
|
|
@@ -420,10 +620,10 @@ async def register_user(
|
|
|
420
620
|
email: str,
|
|
421
621
|
password: str,
|
|
422
622
|
db,
|
|
423
|
-
config:
|
|
424
|
-
extra_data:
|
|
425
|
-
redirect_url:
|
|
426
|
-
) ->
|
|
623
|
+
config: dict[str, Any] | None = None,
|
|
624
|
+
extra_data: dict[str, Any] | None = None,
|
|
625
|
+
redirect_url: str | None = None,
|
|
626
|
+
) -> dict[str, Any]:
|
|
427
627
|
"""
|
|
428
628
|
Handle user registration with automatic token generation.
|
|
429
629
|
|
|
@@ -478,9 +678,7 @@ async def register_user(
|
|
|
478
678
|
)
|
|
479
679
|
|
|
480
680
|
response = _create_registration_response(user_doc, redirect_url)
|
|
481
|
-
set_auth_cookies(
|
|
482
|
-
response, access_token, refresh_token, request=request, config=config
|
|
483
|
-
)
|
|
681
|
+
set_auth_cookies(response, access_token, refresh_token, request=request, config=config)
|
|
484
682
|
|
|
485
683
|
response.set_cookie(
|
|
486
684
|
key="device_id",
|
|
@@ -510,9 +708,7 @@ async def register_user(
|
|
|
510
708
|
return {"success": False, "error": "Registration failed. Please try again."}
|
|
511
709
|
|
|
512
710
|
|
|
513
|
-
async def _get_user_id_from_request(
|
|
514
|
-
request: Request, user_id: Optional[str]
|
|
515
|
-
) -> Optional[str]:
|
|
711
|
+
async def _get_user_id_from_request(request: Request, user_id: str | None) -> str | None:
|
|
516
712
|
"""Extract user_id from request if not provided."""
|
|
517
713
|
if user_id:
|
|
518
714
|
return user_id
|
|
@@ -575,9 +771,7 @@ async def _revoke_session(request: Request) -> None:
|
|
|
575
771
|
await session_mgr.revoke_session_by_refresh_token(refresh_jti)
|
|
576
772
|
|
|
577
773
|
|
|
578
|
-
async def logout_user(
|
|
579
|
-
request: Request, response: Response, user_id: Optional[str] = None
|
|
580
|
-
) -> Response:
|
|
774
|
+
async def logout_user(request: Request, response: Response, user_id: str | None = None) -> Response:
|
|
581
775
|
"""
|
|
582
776
|
Handle user logout with token revocation and cookie clearing.
|
|
583
777
|
|