django-cfg 1.2.31__py3-none-any.whl → 1.3.1__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -10
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -285
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +346 -467
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +40 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -146
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
- django_cfg/apps/payments/managers/__init__.py +0 -23
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -63
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -63
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -122
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -66
- django_cfg/core/integration.py +0 -160
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,294 +1,401 @@
|
|
1
1
|
"""
|
2
|
-
API Access Control Middleware.
|
3
|
-
|
2
|
+
API Access Control Middleware for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Enhanced middleware with service layer integration, smart caching, and graceful degradation.
|
4
5
|
"""
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
import re
|
8
|
+
import time
|
9
|
+
from typing import Optional, Dict, Any, Tuple
|
8
10
|
from django.http import JsonResponse, HttpRequest, HttpResponse
|
9
11
|
from django.utils.deprecation import MiddlewareMixin
|
10
|
-
from django.conf import settings
|
11
12
|
from django.utils import timezone
|
12
|
-
from
|
13
|
-
from
|
14
|
-
|
13
|
+
from django.core.cache import cache
|
14
|
+
from django.contrib.auth import get_user_model
|
15
|
+
|
16
|
+
from ..models import APIKey, Subscription
|
17
|
+
from ..config.helpers import MiddlewareConfigHelper
|
18
|
+
from django_cfg.modules.django_logger import get_logger
|
15
19
|
|
16
|
-
|
20
|
+
User = get_user_model()
|
21
|
+
logger = get_logger("api_access_middleware")
|
17
22
|
|
18
23
|
|
19
24
|
class APIAccessMiddleware(MiddlewareMixin):
|
20
25
|
"""
|
21
|
-
|
26
|
+
Enhanced API Access Control Middleware.
|
22
27
|
|
23
28
|
Features:
|
24
|
-
- API key
|
25
|
-
- Subscription
|
29
|
+
- API key authentication with caching
|
30
|
+
- Subscription validation
|
26
31
|
- Endpoint access control
|
27
|
-
- Usage tracking
|
32
|
+
- Usage tracking and analytics
|
33
|
+
- Rate limiting integration
|
34
|
+
- Graceful degradation
|
35
|
+
- Service layer integration
|
28
36
|
"""
|
29
37
|
|
30
38
|
def __init__(self, get_response=None):
|
31
39
|
super().__init__(get_response)
|
32
|
-
self.api_key_cache = ApiKeyCache()
|
33
|
-
self.rate_limit_cache = RateLimitCache()
|
34
40
|
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
'
|
41
|
-
|
41
|
+
# Load configuration from django-cfg and Constance
|
42
|
+
try:
|
43
|
+
middleware_config = MiddlewareConfigHelper.get_middleware_config()
|
44
|
+
|
45
|
+
# Configuration from django-cfg
|
46
|
+
self.enabled = middleware_config['enabled']
|
47
|
+
self.api_prefixes = middleware_config['api_prefixes']
|
48
|
+
self.exempt_paths = middleware_config['exempt_paths']
|
49
|
+
self.cache_timeout = middleware_config['cache_timeouts']['api_key']
|
50
|
+
|
51
|
+
# Default settings (can be overridden by Constance)
|
52
|
+
self.strict_mode = False
|
53
|
+
self.require_api_key = True
|
54
|
+
|
55
|
+
# Get Constance settings if available
|
56
|
+
constance_settings = middleware_config.get('constance_settings')
|
57
|
+
if constance_settings:
|
58
|
+
# Override with dynamic settings from Constance if needed
|
59
|
+
# For now, we keep static defaults
|
60
|
+
pass
|
61
|
+
|
62
|
+
except Exception as e:
|
63
|
+
logger.warning(f"Failed to load middleware config, using defaults: {e}")
|
64
|
+
# Fallback defaults
|
65
|
+
self.enabled = True
|
66
|
+
self.api_prefixes = ['/api/']
|
67
|
+
self.exempt_paths = ['/api/health/', '/admin/']
|
68
|
+
self.cache_timeout = 300
|
69
|
+
self.strict_mode = False
|
70
|
+
self.require_api_key = True
|
71
|
+
|
72
|
+
# Compile exempt path patterns (static for now)
|
73
|
+
self.exempt_patterns = [
|
74
|
+
re.compile(pattern) for pattern in [
|
75
|
+
r'^/api/payments/[^/]+/status/$',
|
76
|
+
r'^/api/webhooks/[^/]+/$',
|
77
|
+
r'^/api/payments/webhooks/(providers|health|stats)/$', # Admin webhook endpoints
|
78
|
+
r'^/api/currencies/(rates|supported|convert)/$',
|
79
|
+
]
|
80
|
+
]
|
42
81
|
|
43
|
-
|
44
|
-
|
45
|
-
'
|
46
|
-
|
82
|
+
logger.info(f"API Access Middleware initialized", extra={
|
83
|
+
'enabled': self.enabled,
|
84
|
+
'strict_mode': self.strict_mode,
|
85
|
+
'require_api_key': self.require_api_key,
|
86
|
+
'api_prefixes': self.api_prefixes
|
87
|
+
})
|
47
88
|
|
48
89
|
def process_request(self, request: HttpRequest) -> Optional[JsonResponse]:
|
49
|
-
"""
|
90
|
+
"""
|
91
|
+
Process incoming request for API access control.
|
50
92
|
|
51
|
-
|
52
|
-
|
93
|
+
Returns JsonResponse if access should be denied, None to continue.
|
94
|
+
"""
|
95
|
+
if not self.enabled:
|
53
96
|
return None
|
54
97
|
|
55
|
-
#
|
56
|
-
if self.
|
98
|
+
# Check if this path requires authentication
|
99
|
+
if not self._requires_authentication(request.path):
|
57
100
|
return None
|
58
101
|
|
59
|
-
#
|
60
|
-
|
61
|
-
if not api_key:
|
62
|
-
security_error = SecurityError(
|
63
|
-
"API key required for protected endpoint",
|
64
|
-
details={'path': request.path, 'method': request.method}
|
65
|
-
)
|
66
|
-
error_handler.handle_error(security_error, {
|
67
|
-
'middleware': 'api_access',
|
68
|
-
'operation': 'api_key_extraction'
|
69
|
-
}, request)
|
70
|
-
|
71
|
-
return self._error_response(
|
72
|
-
'API key required',
|
73
|
-
status=401,
|
74
|
-
error_code='MISSING_API_KEY'
|
75
|
-
)
|
102
|
+
# Start timing for performance monitoring
|
103
|
+
start_time = time.time()
|
76
104
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
security_error = SecurityError(
|
81
|
-
f"Invalid or expired API key attempted",
|
82
|
-
details={
|
83
|
-
'api_key_prefix': api_key[:8] + '...' if len(api_key) > 8 else api_key,
|
84
|
-
'path': request.path,
|
85
|
-
'method': request.method,
|
86
|
-
'ip_address': self._get_client_ip(request)
|
87
|
-
}
|
88
|
-
)
|
89
|
-
error_handler.handle_error(security_error, {
|
90
|
-
'middleware': 'api_access',
|
91
|
-
'operation': 'api_key_validation'
|
92
|
-
}, request)
|
105
|
+
try:
|
106
|
+
# Extract API key from request
|
107
|
+
api_key_value = self._extract_api_key(request)
|
93
108
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
if not subscription:
|
105
|
-
return self._error_response(
|
106
|
-
f'No active subscription for {endpoint_group.display_name}',
|
107
|
-
status=403,
|
108
|
-
error_code='NO_SUBSCRIPTION'
|
109
|
-
)
|
109
|
+
if not api_key_value:
|
110
|
+
if self.require_api_key:
|
111
|
+
return self._create_error_response(
|
112
|
+
'API key required',
|
113
|
+
'missing_api_key',
|
114
|
+
401
|
115
|
+
)
|
116
|
+
else:
|
117
|
+
# API key not required, continue without authentication
|
118
|
+
return None
|
110
119
|
|
111
|
-
#
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
120
|
+
# Validate API key
|
121
|
+
api_key, validation_result = self._validate_api_key(api_key_value)
|
122
|
+
|
123
|
+
if not validation_result['valid']:
|
124
|
+
return self._create_error_response(
|
125
|
+
validation_result['message'],
|
126
|
+
validation_result['error_code'],
|
127
|
+
401
|
117
128
|
)
|
118
129
|
|
119
|
-
#
|
120
|
-
|
130
|
+
# Check subscription access
|
131
|
+
subscription_result = self._check_subscription_access(api_key, request.path)
|
132
|
+
|
133
|
+
if not subscription_result['allowed']:
|
134
|
+
if self.strict_mode:
|
135
|
+
return self._create_error_response(
|
136
|
+
subscription_result['message'],
|
137
|
+
subscription_result['error_code'],
|
138
|
+
403
|
139
|
+
)
|
140
|
+
else:
|
141
|
+
# Non-strict mode: add warning but allow access
|
142
|
+
request.subscription_warning = subscription_result
|
143
|
+
|
144
|
+
# Add authentication info to request
|
145
|
+
request.api_key = api_key
|
146
|
+
request.authenticated_user = api_key.user
|
147
|
+
request.subscription_access = subscription_result
|
148
|
+
|
149
|
+
# Track usage (async to avoid blocking)
|
150
|
+
self._track_usage_async(api_key, request)
|
151
|
+
|
152
|
+
# Log successful authentication
|
153
|
+
processing_time = (time.time() - start_time) * 1000 # ms
|
154
|
+
logger.debug(f"API access granted", extra={
|
155
|
+
'api_key_id': str(api_key.id),
|
156
|
+
'user_id': api_key.user.id,
|
157
|
+
'path': request.path,
|
158
|
+
'processing_time_ms': round(processing_time, 2)
|
159
|
+
})
|
160
|
+
|
161
|
+
return None # Continue processing
|
162
|
+
|
163
|
+
except Exception as e:
|
164
|
+
logger.error(f"API access middleware error", extra={
|
165
|
+
'path': request.path,
|
166
|
+
'error': str(e),
|
167
|
+
'processing_time_ms': round((time.time() - start_time) * 1000, 2)
|
168
|
+
})
|
169
|
+
|
170
|
+
if self.strict_mode:
|
171
|
+
return self._create_error_response(
|
172
|
+
'Authentication service unavailable',
|
173
|
+
'service_error',
|
174
|
+
503
|
175
|
+
)
|
176
|
+
else:
|
177
|
+
# Graceful degradation: allow access but log the issue
|
178
|
+
return None
|
179
|
+
|
180
|
+
def _requires_authentication(self, path: str) -> bool:
|
181
|
+
"""
|
182
|
+
Check if the given path requires API authentication.
|
183
|
+
"""
|
184
|
+
# Check if path starts with API prefix
|
185
|
+
requires_auth = any(path.startswith(prefix) for prefix in self.api_prefixes)
|
121
186
|
|
122
|
-
|
123
|
-
|
124
|
-
request.payment_user = api_key_obj.user
|
187
|
+
if not requires_auth:
|
188
|
+
return False
|
125
189
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
"""Process response to track API usage."""
|
190
|
+
# Check exempt paths
|
191
|
+
if path in self.exempt_paths:
|
192
|
+
return False
|
130
193
|
|
131
|
-
#
|
132
|
-
|
133
|
-
|
194
|
+
# Check exempt patterns
|
195
|
+
for pattern in self.exempt_patterns:
|
196
|
+
if pattern.match(path):
|
197
|
+
return False
|
134
198
|
|
135
|
-
return
|
136
|
-
|
137
|
-
def _is_api_request(self, request: HttpRequest) -> bool:
|
138
|
-
"""Check if request is an API request."""
|
139
|
-
path = request.path
|
140
|
-
return any(path.startswith(prefix) for prefix in self.api_prefixes)
|
141
|
-
|
142
|
-
def _is_exempt_path(self, request: HttpRequest) -> bool:
|
143
|
-
"""Check if path is exempt from API key requirement."""
|
144
|
-
path = request.path
|
145
|
-
return any(path.startswith(exempt) for exempt in self.exempt_paths)
|
199
|
+
return True
|
146
200
|
|
147
201
|
def _extract_api_key(self, request: HttpRequest) -> Optional[str]:
|
148
|
-
"""
|
202
|
+
"""
|
203
|
+
Extract API key from request headers or query parameters.
|
149
204
|
|
150
|
-
|
205
|
+
Supports multiple formats:
|
206
|
+
- Authorization: Bearer <key>
|
207
|
+
- Authorization: ApiKey <key>
|
208
|
+
- X-API-Key: <key>
|
209
|
+
- api_key query parameter
|
210
|
+
"""
|
211
|
+
# Check Authorization header
|
151
212
|
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
213
|
+
|
152
214
|
if auth_header.startswith('Bearer '):
|
153
|
-
return auth_header[7:] # Remove 'Bearer '
|
215
|
+
return auth_header[7:] # Remove 'Bearer '
|
216
|
+
elif auth_header.startswith('ApiKey '):
|
217
|
+
return auth_header[7:] # Remove 'ApiKey '
|
154
218
|
|
155
|
-
#
|
156
|
-
|
157
|
-
if
|
158
|
-
return
|
219
|
+
# Check X-API-Key header
|
220
|
+
api_key_header = request.META.get('HTTP_X_API_KEY')
|
221
|
+
if api_key_header:
|
222
|
+
return api_key_header
|
159
223
|
|
160
|
-
#
|
161
|
-
|
162
|
-
if
|
163
|
-
|
224
|
+
# Check query parameter (less secure, but supported)
|
225
|
+
api_key_param = request.GET.get('api_key')
|
226
|
+
if api_key_param:
|
227
|
+
logger.warning(f"API key provided via query parameter", extra={
|
228
|
+
'path': request.path,
|
229
|
+
'ip': self._get_client_ip(request)
|
230
|
+
})
|
231
|
+
return api_key_param
|
164
232
|
|
165
233
|
return None
|
166
234
|
|
167
|
-
def _validate_api_key(self,
|
168
|
-
"""
|
235
|
+
def _validate_api_key(self, api_key_value: str) -> Tuple[Optional[APIKey], Dict[str, Any]]:
|
236
|
+
"""
|
237
|
+
Validate API key with caching.
|
169
238
|
|
239
|
+
Returns tuple of (APIKey instance, validation result dict).
|
240
|
+
"""
|
241
|
+
# Check cache first
|
242
|
+
cache_key = f"api_key_validation:{api_key_value[:10]}..." # Partial key for security
|
243
|
+
cached_result = cache.get(cache_key)
|
244
|
+
|
245
|
+
if cached_result:
|
246
|
+
if cached_result['valid']:
|
247
|
+
try:
|
248
|
+
api_key = APIKey.objects.get(id=cached_result['api_key_id'])
|
249
|
+
return api_key, cached_result
|
250
|
+
except APIKey.DoesNotExist:
|
251
|
+
# Cache is stale, continue with fresh validation
|
252
|
+
pass
|
253
|
+
else:
|
254
|
+
# Return cached negative result
|
255
|
+
return None, cached_result
|
256
|
+
|
257
|
+
# Fresh validation
|
170
258
|
try:
|
171
|
-
|
172
|
-
cached_key = self.redis_service.get_api_key(api_key)
|
173
|
-
if cached_key:
|
174
|
-
return cached_key
|
259
|
+
api_key = APIKey.get_valid_key(api_key_value)
|
175
260
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
# Cache valid key
|
184
|
-
self.redis_service.cache_api_key(api_key_obj)
|
261
|
+
if api_key:
|
262
|
+
result = {
|
263
|
+
'valid': True,
|
264
|
+
'api_key_id': str(api_key.id),
|
265
|
+
'user_id': api_key.user.id,
|
266
|
+
'message': 'API key valid'
|
267
|
+
}
|
185
268
|
|
186
|
-
#
|
187
|
-
|
269
|
+
# Cache positive result
|
270
|
+
cache.set(cache_key, result, timeout=self.cache_timeout)
|
271
|
+
|
272
|
+
return api_key, result
|
273
|
+
else:
|
274
|
+
result = {
|
275
|
+
'valid': False,
|
276
|
+
'message': 'Invalid or expired API key',
|
277
|
+
'error_code': 'invalid_api_key'
|
278
|
+
}
|
279
|
+
|
280
|
+
# Cache negative result for shorter time
|
281
|
+
cache.set(cache_key, result, timeout=60) # 1 minute
|
282
|
+
|
283
|
+
return None, result
|
188
284
|
|
189
|
-
return api_key_obj
|
190
|
-
|
191
|
-
return None
|
192
|
-
|
193
285
|
except Exception as e:
|
194
|
-
logger.error(f"
|
195
|
-
return None
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
# This would be customized per project
|
201
|
-
# For now, return None (no endpoint group restrictions)
|
202
|
-
# In real implementation, this would map URL patterns to endpoint groups
|
203
|
-
|
204
|
-
path = request.path
|
205
|
-
|
206
|
-
# Example mapping (would be configurable)
|
207
|
-
endpoint_mappings = {
|
208
|
-
'/api/v1/payments/': 'payments',
|
209
|
-
'/api/v1/subscriptions/': 'billing',
|
210
|
-
'/api/v1/users/': 'user_management',
|
211
|
-
}
|
212
|
-
|
213
|
-
for path_prefix, group_name in endpoint_mappings.items():
|
214
|
-
if path.startswith(path_prefix):
|
215
|
-
try:
|
216
|
-
return EndpointGroup.objects.get(name=group_name, is_active=True)
|
217
|
-
except EndpointGroup.DoesNotExist:
|
218
|
-
continue
|
219
|
-
|
220
|
-
return None
|
286
|
+
logger.error(f"API key validation error: {e}")
|
287
|
+
return None, {
|
288
|
+
'valid': False,
|
289
|
+
'message': 'API key validation failed',
|
290
|
+
'error_code': 'validation_error'
|
291
|
+
}
|
221
292
|
|
222
|
-
def _check_subscription_access(self,
|
223
|
-
"""
|
224
|
-
|
293
|
+
def _check_subscription_access(self, api_key: APIKey, path: str) -> Dict[str, Any]:
|
294
|
+
"""
|
295
|
+
Check if user has valid subscription for the requested endpoint.
|
296
|
+
"""
|
225
297
|
try:
|
226
|
-
# Get active
|
227
|
-
|
228
|
-
user=user,
|
229
|
-
|
230
|
-
status='active',
|
298
|
+
# Get user's active subscriptions
|
299
|
+
active_subscriptions = Subscription.objects.filter(
|
300
|
+
user=api_key.user,
|
301
|
+
status=Subscription.SubscriptionStatus.ACTIVE,
|
231
302
|
expires_at__gt=timezone.now()
|
232
|
-
).
|
303
|
+
).select_related('tariff', 'endpoint_group')
|
233
304
|
|
234
|
-
|
305
|
+
if not active_subscriptions.exists():
|
306
|
+
return {
|
307
|
+
'allowed': False,
|
308
|
+
'message': 'No active subscription found',
|
309
|
+
'error_code': 'no_active_subscription',
|
310
|
+
'upgrade_required': True
|
311
|
+
}
|
235
312
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
def _is_usage_exceeded(self, subscription: Subscription) -> bool:
|
241
|
-
"""Check if subscription usage limit is exceeded."""
|
242
|
-
|
243
|
-
try:
|
244
|
-
# Check current usage against limit
|
245
|
-
if subscription.usage_limit == 0: # Unlimited
|
246
|
-
return False
|
313
|
+
# For now, allow access if user has any active subscription
|
314
|
+
# TODO: Implement endpoint-specific access control based on tariff/endpoint_group
|
315
|
+
|
316
|
+
subscription = active_subscriptions.first()
|
247
317
|
|
248
|
-
return
|
318
|
+
return {
|
319
|
+
'allowed': True,
|
320
|
+
'subscription_id': str(subscription.id),
|
321
|
+
'tier': subscription.tier,
|
322
|
+
'tariff_name': subscription.tariff.name if subscription.tariff else None,
|
323
|
+
'requests_remaining': subscription.requests_remaining(),
|
324
|
+
'expires_at': subscription.expires_at.isoformat() if subscription.expires_at else None
|
325
|
+
}
|
249
326
|
|
250
327
|
except Exception as e:
|
251
|
-
logger.error(f"
|
252
|
-
return
|
328
|
+
logger.error(f"Subscription access check error: {e}")
|
329
|
+
return {
|
330
|
+
'allowed': not self.strict_mode, # Allow in non-strict mode
|
331
|
+
'message': 'Subscription check failed',
|
332
|
+
'error_code': 'subscription_check_error'
|
333
|
+
}
|
253
334
|
|
254
|
-
def
|
255
|
-
"""
|
256
|
-
|
335
|
+
def _track_usage_async(self, api_key: APIKey, request: HttpRequest):
|
336
|
+
"""
|
337
|
+
Track API usage asynchronously to avoid blocking the request.
|
338
|
+
"""
|
257
339
|
try:
|
258
|
-
# Increment
|
259
|
-
|
260
|
-
subscription.save(update_fields=['usage_current'])
|
340
|
+
# Increment usage counter (this will trigger signals)
|
341
|
+
api_key.increment_usage(ip_address=self._get_client_ip(request))
|
261
342
|
|
262
|
-
# Update
|
263
|
-
|
343
|
+
# Update usage analytics in cache
|
344
|
+
today = timezone.now().date().isoformat()
|
264
345
|
|
265
|
-
#
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
)
|
346
|
+
# Daily usage counter
|
347
|
+
daily_key = f"api_usage_daily:{api_key.user.id}:{today}"
|
348
|
+
cache.set(daily_key, cache.get(daily_key, 0) + 1, timeout=86400 * 2)
|
349
|
+
|
350
|
+
# Endpoint usage counter
|
351
|
+
endpoint_key = f"endpoint_usage:{request.path}:{today}"
|
352
|
+
cache.set(endpoint_key, cache.get(endpoint_key, 0) + 1, timeout=86400 * 2)
|
272
353
|
|
273
354
|
except Exception as e:
|
274
|
-
logger.
|
355
|
+
logger.warning(f"Usage tracking failed: {e}")
|
275
356
|
|
276
357
|
def _get_client_ip(self, request: HttpRequest) -> str:
|
277
|
-
"""
|
358
|
+
"""
|
359
|
+
Get client IP address from request.
|
360
|
+
"""
|
278
361
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
279
362
|
if x_forwarded_for:
|
280
|
-
|
281
|
-
else:
|
282
|
-
ip = request.META.get('REMOTE_ADDR', '')
|
283
|
-
return ip
|
284
|
-
|
285
|
-
def _error_response(self, message: str, status: int = 400, error_code: str = 'ERROR') -> JsonResponse:
|
286
|
-
"""Return standardized error response."""
|
363
|
+
return x_forwarded_for.split(',')[0].strip()
|
287
364
|
|
365
|
+
x_real_ip = request.META.get('HTTP_X_REAL_IP')
|
366
|
+
if x_real_ip:
|
367
|
+
return x_real_ip
|
368
|
+
|
369
|
+
return request.META.get('REMOTE_ADDR', 'unknown')
|
370
|
+
|
371
|
+
def _create_error_response(self, message: str, error_code: str, status_code: int) -> JsonResponse:
|
372
|
+
"""
|
373
|
+
Create standardized error response.
|
374
|
+
"""
|
288
375
|
return JsonResponse({
|
289
|
-
'
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
376
|
+
'success': False,
|
377
|
+
'error': message,
|
378
|
+
'error_code': error_code,
|
379
|
+
'timestamp': timezone.now().isoformat()
|
380
|
+
}, status=status_code)
|
381
|
+
|
382
|
+
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
383
|
+
"""
|
384
|
+
Process response to add headers and perform cleanup.
|
385
|
+
"""
|
386
|
+
# Add API usage headers if authenticated
|
387
|
+
if hasattr(request, 'api_key') and hasattr(request, 'subscription_access'):
|
388
|
+
subscription_access = request.subscription_access
|
389
|
+
|
390
|
+
if subscription_access.get('allowed'):
|
391
|
+
response['X-RateLimit-Remaining'] = str(subscription_access.get('requests_remaining', 'unlimited'))
|
392
|
+
response['X-Subscription-Tier'] = subscription_access.get('tier', 'unknown')
|
393
|
+
|
394
|
+
if subscription_access.get('expires_at'):
|
395
|
+
response['X-Subscription-Expires'] = subscription_access['expires_at']
|
396
|
+
|
397
|
+
# Add security headers
|
398
|
+
response['X-API-Version'] = '2.0'
|
399
|
+
response['X-Powered-By'] = 'Universal Payment System'
|
400
|
+
|
401
|
+
return response
|