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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cookie Security Utilities
|
|
3
|
+
|
|
4
|
+
Provides secure cookie configuration helpers based on manifest settings and environment.
|
|
5
|
+
|
|
6
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from fastapi import Request
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_secure_cookie_settings(
|
|
19
|
+
request: Request, config: Optional[Dict[str, Any]] = None
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Get secure cookie settings based on manifest config and request environment.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
request: FastAPI Request object
|
|
26
|
+
config: Optional token_management config from manifest (if None, uses defaults)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dictionary of cookie settings for FastAPI response.set_cookie()
|
|
30
|
+
"""
|
|
31
|
+
# Default settings
|
|
32
|
+
secure = False
|
|
33
|
+
httponly = True
|
|
34
|
+
samesite = "lax"
|
|
35
|
+
|
|
36
|
+
# Get security config from token_management
|
|
37
|
+
if config:
|
|
38
|
+
security = config.get("security", {})
|
|
39
|
+
|
|
40
|
+
# HttpOnly flag
|
|
41
|
+
httponly = security.get("cookie_httponly", True)
|
|
42
|
+
|
|
43
|
+
# SameSite flag
|
|
44
|
+
samesite_str = security.get("cookie_samesite", "lax")
|
|
45
|
+
samesite = samesite_str.lower()
|
|
46
|
+
|
|
47
|
+
# Secure flag - determine based on config and environment
|
|
48
|
+
cookie_secure = security.get("cookie_secure", "auto")
|
|
49
|
+
|
|
50
|
+
if cookie_secure == "auto":
|
|
51
|
+
# Auto-detect: secure if HTTPS or production environment
|
|
52
|
+
is_https = request.url.scheme == "https"
|
|
53
|
+
is_production = (
|
|
54
|
+
os.getenv("G_NOME_ENV") == "production"
|
|
55
|
+
or os.getenv("ENVIRONMENT") == "production"
|
|
56
|
+
)
|
|
57
|
+
secure = is_https or is_production
|
|
58
|
+
elif cookie_secure == "true":
|
|
59
|
+
secure = True
|
|
60
|
+
else:
|
|
61
|
+
secure = False
|
|
62
|
+
else:
|
|
63
|
+
# No config - use environment-based defaults
|
|
64
|
+
is_https = request.url.scheme == "https"
|
|
65
|
+
is_production = (
|
|
66
|
+
os.getenv("G_NOME_ENV") == "production"
|
|
67
|
+
or os.getenv("ENVIRONMENT") == "production"
|
|
68
|
+
)
|
|
69
|
+
secure = is_https or is_production
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"httponly": httponly,
|
|
73
|
+
"secure": secure,
|
|
74
|
+
"samesite": samesite,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_auth_cookies(
|
|
79
|
+
response,
|
|
80
|
+
access_token: str,
|
|
81
|
+
refresh_token: Optional[str] = None,
|
|
82
|
+
request: Optional[Request] = None,
|
|
83
|
+
config: Optional[Dict[str, Any]] = None,
|
|
84
|
+
access_token_ttl: Optional[int] = None,
|
|
85
|
+
refresh_token_ttl: Optional[int] = None,
|
|
86
|
+
):
|
|
87
|
+
"""
|
|
88
|
+
Set authentication cookies on a response with secure settings.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
response: FastAPI Response object
|
|
92
|
+
access_token: Access token to set in cookie
|
|
93
|
+
refresh_token: Optional refresh token to set in cookie
|
|
94
|
+
request: Optional Request object for environment detection
|
|
95
|
+
config: Optional token_management config from manifest
|
|
96
|
+
access_token_ttl: Optional access token TTL in seconds (from config if not provided)
|
|
97
|
+
refresh_token_ttl: Optional refresh token TTL in seconds (from config if not provided)
|
|
98
|
+
"""
|
|
99
|
+
# Get cookie settings
|
|
100
|
+
if request:
|
|
101
|
+
cookie_settings = get_secure_cookie_settings(request, config)
|
|
102
|
+
else:
|
|
103
|
+
cookie_settings = {
|
|
104
|
+
"httponly": True,
|
|
105
|
+
"secure": os.getenv("G_NOME_ENV") == "production",
|
|
106
|
+
"samesite": "lax",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Get TTLs
|
|
110
|
+
if access_token_ttl is None and config:
|
|
111
|
+
access_token_ttl = config.get("access_token_ttl", 900)
|
|
112
|
+
elif access_token_ttl is None:
|
|
113
|
+
access_token_ttl = 900 # Default 15 minutes
|
|
114
|
+
|
|
115
|
+
if refresh_token_ttl is None and config:
|
|
116
|
+
refresh_token_ttl = config.get("refresh_token_ttl", 604800)
|
|
117
|
+
elif refresh_token_ttl is None:
|
|
118
|
+
refresh_token_ttl = 604800 # Default 7 days
|
|
119
|
+
|
|
120
|
+
# Set access token cookie
|
|
121
|
+
response.set_cookie(
|
|
122
|
+
key="token", value=access_token, max_age=access_token_ttl, **cookie_settings
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Set refresh token cookie if provided
|
|
126
|
+
if refresh_token:
|
|
127
|
+
response.set_cookie(
|
|
128
|
+
key="refresh_token",
|
|
129
|
+
value=refresh_token,
|
|
130
|
+
max_age=refresh_token_ttl,
|
|
131
|
+
**cookie_settings,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def clear_auth_cookies(response, request: Optional[Request] = None):
|
|
136
|
+
"""
|
|
137
|
+
Clear authentication cookies from response.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
response: FastAPI Response object
|
|
141
|
+
request: Optional Request object for environment detection
|
|
142
|
+
"""
|
|
143
|
+
# Get cookie settings for samesite (needed for deletion)
|
|
144
|
+
if request:
|
|
145
|
+
cookie_settings = get_secure_cookie_settings(request)
|
|
146
|
+
samesite = cookie_settings.get("samesite", "lax")
|
|
147
|
+
secure = cookie_settings.get("secure", False)
|
|
148
|
+
else:
|
|
149
|
+
samesite = "lax"
|
|
150
|
+
secure = os.getenv("G_NOME_ENV") == "production"
|
|
151
|
+
|
|
152
|
+
# Delete access token cookie
|
|
153
|
+
response.delete_cookie(key="token", httponly=True, secure=secure, samesite=samesite)
|
|
154
|
+
|
|
155
|
+
# Delete refresh token cookie
|
|
156
|
+
response.delete_cookie(
|
|
157
|
+
key="refresh_token", httponly=True, secure=secure, samesite=samesite
|
|
158
|
+
)
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication Decorators
|
|
3
|
+
|
|
4
|
+
Decorators for simplifying authentication and security enforcement.
|
|
5
|
+
|
|
6
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from fastapi import HTTPException, Request, status
|
|
16
|
+
from fastapi.responses import RedirectResponse
|
|
17
|
+
|
|
18
|
+
from .dependencies import get_current_user_from_request
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Rate limiting storage (in-memory, can be replaced with Redis for distributed systems)
|
|
23
|
+
_rate_limit_storage: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def require_auth(redirect_to: str = "/login"):
|
|
27
|
+
"""
|
|
28
|
+
Decorator for routes requiring authentication.
|
|
29
|
+
|
|
30
|
+
Automatically injects user into request.state.user and redirects to login if not authenticated.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
@app.get("/dashboard")
|
|
34
|
+
@require_auth()
|
|
35
|
+
async def dashboard(request: Request):
|
|
36
|
+
user = request.state.user # Automatically available
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
redirect_to: URL to redirect to if not authenticated (default: "/login")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
44
|
+
@wraps(func)
|
|
45
|
+
async def wrapper(request: Request, *args, **kwargs):
|
|
46
|
+
user = await get_current_user_from_request(request)
|
|
47
|
+
if not user:
|
|
48
|
+
# Check if it's an API request (JSON) or web request
|
|
49
|
+
accept = request.headers.get("accept", "")
|
|
50
|
+
if "application/json" in accept:
|
|
51
|
+
raise HTTPException(
|
|
52
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
53
|
+
detail="Authentication required",
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
# Web request - redirect
|
|
57
|
+
return RedirectResponse(url=redirect_to, status_code=302)
|
|
58
|
+
|
|
59
|
+
# Inject user into request state
|
|
60
|
+
request.state.user = user
|
|
61
|
+
|
|
62
|
+
return await func(request, *args, **kwargs)
|
|
63
|
+
|
|
64
|
+
return wrapper
|
|
65
|
+
|
|
66
|
+
return decorator
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_production_environment() -> bool:
|
|
70
|
+
"""Check if running in production environment."""
|
|
71
|
+
import os
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
os.getenv("G_NOME_ENV") == "production"
|
|
75
|
+
or os.getenv("ENVIRONMENT") == "production"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _validate_https(request: Request) -> None:
|
|
80
|
+
"""Validate HTTPS requirement in production."""
|
|
81
|
+
if _is_production_environment() and request.url.scheme != "https":
|
|
82
|
+
raise HTTPException(
|
|
83
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
84
|
+
detail="HTTPS required in production",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def _get_csrf_token(request: Request) -> Optional[str]:
|
|
89
|
+
"""Extract CSRF token from request headers or form data."""
|
|
90
|
+
csrf_token = request.headers.get("X-CSRF-Token")
|
|
91
|
+
if csrf_token:
|
|
92
|
+
return csrf_token
|
|
93
|
+
|
|
94
|
+
# Try to get from form data if not in headers
|
|
95
|
+
try:
|
|
96
|
+
form_data = await request.form()
|
|
97
|
+
return form_data.get("csrf_token")
|
|
98
|
+
except (RuntimeError, ValueError):
|
|
99
|
+
# Type 2: Recoverable - form parsing failed, return None
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_state_changing_method(method: str) -> bool:
|
|
104
|
+
"""Check if HTTP method is state-changing."""
|
|
105
|
+
return method in ["POST", "PUT", "DELETE", "PATCH"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def _validate_csrf_token(request: Request) -> None:
|
|
109
|
+
"""Validate CSRF token for state-changing requests."""
|
|
110
|
+
csrf_token = await _get_csrf_token(request)
|
|
111
|
+
session_csrf = request.cookies.get("csrf_token")
|
|
112
|
+
|
|
113
|
+
# Only validate CSRF if a session token exists
|
|
114
|
+
# If no session token exists yet (e.g., first registration), allow the request
|
|
115
|
+
if session_csrf and (not csrf_token or csrf_token != session_csrf):
|
|
116
|
+
raise HTTPException(
|
|
117
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
118
|
+
detail="Invalid or missing CSRF token",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def token_security(enforce_https: bool = True, check_csrf: bool = True):
|
|
123
|
+
"""
|
|
124
|
+
Decorator to enforce security settings from manifest.
|
|
125
|
+
|
|
126
|
+
Validates HTTPS in production, CSRF tokens, and secure cookie enforcement.
|
|
127
|
+
|
|
128
|
+
Usage:
|
|
129
|
+
@app.post("/api/data")
|
|
130
|
+
@token_security()
|
|
131
|
+
async def update_data(request: Request):
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
enforce_https: Enforce HTTPS in production (default: True)
|
|
136
|
+
check_csrf: Check CSRF tokens (default: True)
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
140
|
+
@wraps(func)
|
|
141
|
+
async def wrapper(request: Request, *args, **kwargs):
|
|
142
|
+
if enforce_https:
|
|
143
|
+
_validate_https(request)
|
|
144
|
+
|
|
145
|
+
if check_csrf and _is_state_changing_method(request.method):
|
|
146
|
+
await _validate_csrf_token(request)
|
|
147
|
+
|
|
148
|
+
return await func(request, *args, **kwargs)
|
|
149
|
+
|
|
150
|
+
return wrapper
|
|
151
|
+
|
|
152
|
+
return decorator
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def rate_limit_auth(
|
|
156
|
+
endpoint: str = "login",
|
|
157
|
+
max_attempts: Optional[int] = None,
|
|
158
|
+
window_seconds: Optional[int] = None,
|
|
159
|
+
):
|
|
160
|
+
"""
|
|
161
|
+
Rate limiting decorator for auth endpoints.
|
|
162
|
+
|
|
163
|
+
Tracks attempts by IP + email and returns 429 when exceeded.
|
|
164
|
+
If max_attempts/window_seconds not provided, reads from manifest config.
|
|
165
|
+
|
|
166
|
+
Usage:
|
|
167
|
+
@app.post("/login")
|
|
168
|
+
@rate_limit_auth(endpoint="login")
|
|
169
|
+
async def login(request: Request, email: str, password: str):
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
endpoint: Endpoint identifier for rate limiting (default: "login")
|
|
174
|
+
max_attempts: Maximum attempts allowed (default: from manifest config or 5)
|
|
175
|
+
window_seconds: Time window in seconds (default: from manifest config or 300)
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
179
|
+
@wraps(func)
|
|
180
|
+
async def wrapper(request: Request, *args, **kwargs):
|
|
181
|
+
# Get rate limit config from manifest if available
|
|
182
|
+
config = getattr(request.state, "token_management_config", None)
|
|
183
|
+
rate_limit_config = None
|
|
184
|
+
|
|
185
|
+
if config:
|
|
186
|
+
security = config.get("security", {})
|
|
187
|
+
rate_limiting = security.get("rate_limiting", {})
|
|
188
|
+
rate_limit_config = rate_limiting.get(endpoint)
|
|
189
|
+
|
|
190
|
+
# Use provided values or config values or defaults
|
|
191
|
+
if max_attempts is None:
|
|
192
|
+
max_attempts_val = (
|
|
193
|
+
rate_limit_config.get("max_attempts") if rate_limit_config else 5
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
max_attempts_val = max_attempts
|
|
197
|
+
|
|
198
|
+
if window_seconds is None:
|
|
199
|
+
window_seconds_val = (
|
|
200
|
+
rate_limit_config.get("window_seconds")
|
|
201
|
+
if rate_limit_config
|
|
202
|
+
else 300
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
window_seconds_val = window_seconds
|
|
206
|
+
|
|
207
|
+
# Get identifier (IP + email if available)
|
|
208
|
+
ip_address = request.client.host if request.client else "unknown"
|
|
209
|
+
email = kwargs.get("email") or (
|
|
210
|
+
await request.form() if request.method == "POST" else {}
|
|
211
|
+
).get("email", "")
|
|
212
|
+
|
|
213
|
+
identifier = f"{endpoint}:{ip_address}:{email}"
|
|
214
|
+
current_time = time.time()
|
|
215
|
+
|
|
216
|
+
# Clean old entries
|
|
217
|
+
if identifier in _rate_limit_storage:
|
|
218
|
+
attempts = _rate_limit_storage[identifier]
|
|
219
|
+
# Remove old attempts outside window
|
|
220
|
+
_rate_limit_storage[identifier] = {
|
|
221
|
+
ts: count
|
|
222
|
+
for ts, count in attempts.items()
|
|
223
|
+
if current_time - ts < window_seconds_val
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Count attempts in window
|
|
227
|
+
attempts_in_window = sum(_rate_limit_storage[identifier].values())
|
|
228
|
+
|
|
229
|
+
if attempts_in_window >= max_attempts_val:
|
|
230
|
+
logger.warning(
|
|
231
|
+
f"Rate limit exceeded for {identifier}: "
|
|
232
|
+
f"{attempts_in_window} attempts in {window_seconds_val}s"
|
|
233
|
+
)
|
|
234
|
+
raise HTTPException(
|
|
235
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
236
|
+
detail=f"Too many attempts. Please try again in {window_seconds_val} seconds.",
|
|
237
|
+
headers={"Retry-After": str(window_seconds_val)},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Record this attempt
|
|
241
|
+
if identifier not in _rate_limit_storage:
|
|
242
|
+
_rate_limit_storage[identifier] = {}
|
|
243
|
+
_rate_limit_storage[identifier][current_time] = 1
|
|
244
|
+
|
|
245
|
+
return await func(request, *args, **kwargs)
|
|
246
|
+
|
|
247
|
+
return wrapper
|
|
248
|
+
|
|
249
|
+
return decorator
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def auto_token_setup(func: Optional[Callable[..., Awaitable[Any]]] = None):
|
|
253
|
+
"""
|
|
254
|
+
Decorator to automatically set up tokens on successful login/register.
|
|
255
|
+
|
|
256
|
+
This decorator wraps login/register functions and automatically:
|
|
257
|
+
- Generates token pair
|
|
258
|
+
- Sets cookies with correct security settings
|
|
259
|
+
- Creates session if enabled
|
|
260
|
+
- Reads config from manifest
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
@app.post("/login")
|
|
264
|
+
@auto_token_setup
|
|
265
|
+
async def login(request: Request, email: str, password: str):
|
|
266
|
+
# Your login logic that returns user dict
|
|
267
|
+
user = await authenticate_user(email, password)
|
|
268
|
+
return {"user": user} # Decorator handles token setup
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def decorator(f: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
272
|
+
@wraps(f)
|
|
273
|
+
async def wrapper(request: Request, *args, **kwargs):
|
|
274
|
+
# Call original function
|
|
275
|
+
result = await f(request, *args, **kwargs)
|
|
276
|
+
|
|
277
|
+
# If result contains user, set up tokens
|
|
278
|
+
if isinstance(result, dict) and "user" in result:
|
|
279
|
+
try:
|
|
280
|
+
from .cookie_utils import set_auth_cookies
|
|
281
|
+
from .dependencies import SECRET_KEY, get_session_manager
|
|
282
|
+
from .jwt import generate_token_pair
|
|
283
|
+
from .utils import get_device_info
|
|
284
|
+
|
|
285
|
+
user = result["user"]
|
|
286
|
+
user_data = {
|
|
287
|
+
"user_id": str(user.get("_id") or user.get("user_id")),
|
|
288
|
+
"email": user.get("email"),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Get device info
|
|
292
|
+
device_info = get_device_info(request)
|
|
293
|
+
|
|
294
|
+
# Generate token pair
|
|
295
|
+
access_token, refresh_token, token_metadata = generate_token_pair(
|
|
296
|
+
user_data, str(SECRET_KEY), device_info=device_info
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Create session if available
|
|
300
|
+
session_mgr = await get_session_manager(request)
|
|
301
|
+
if session_mgr:
|
|
302
|
+
await session_mgr.create_session(
|
|
303
|
+
user_id=user_data["email"],
|
|
304
|
+
device_id=device_info["device_id"],
|
|
305
|
+
refresh_jti=token_metadata.get("refresh_jti"),
|
|
306
|
+
device_info=device_info,
|
|
307
|
+
ip_address=device_info.get("ip_address"),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Get config from request state or manifest
|
|
311
|
+
config = getattr(request.state, "token_management_config", None)
|
|
312
|
+
|
|
313
|
+
# Create response if not already a response
|
|
314
|
+
if not hasattr(result, "set_cookie"):
|
|
315
|
+
from fastapi.responses import JSONResponse
|
|
316
|
+
|
|
317
|
+
response = JSONResponse(result)
|
|
318
|
+
else:
|
|
319
|
+
response = result
|
|
320
|
+
|
|
321
|
+
# Set cookies
|
|
322
|
+
set_auth_cookies(
|
|
323
|
+
response,
|
|
324
|
+
access_token,
|
|
325
|
+
refresh_token,
|
|
326
|
+
request=request,
|
|
327
|
+
config=config,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return response
|
|
331
|
+
except (
|
|
332
|
+
ValueError,
|
|
333
|
+
TypeError,
|
|
334
|
+
AttributeError,
|
|
335
|
+
KeyError,
|
|
336
|
+
RuntimeError,
|
|
337
|
+
) as e:
|
|
338
|
+
logger.error(f"Error in auto_token_setup: {e}", exc_info=True)
|
|
339
|
+
# Return original result if token setup fails
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
return wrapper
|
|
345
|
+
|
|
346
|
+
# Support both @auto_token_setup and @auto_token_setup()
|
|
347
|
+
if func is None:
|
|
348
|
+
return decorator
|
|
349
|
+
else:
|
|
350
|
+
return decorator(func)
|