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.
Files changed (256) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -10
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +526 -222
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +465 -70
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  39. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  43. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +13 -18
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +172 -148
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -285
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +346 -467
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +234 -174
  74. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  75. django_cfg/apps/payments/services/providers/registry.py +367 -301
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  85. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +45 -48
  93. django_cfg/apps/payments/urls_admin.py +33 -42
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/config.py +1 -1
  110. django_cfg/core/config.py +40 -4
  111. django_cfg/core/generation.py +25 -4
  112. django_cfg/core/integration/README.md +363 -0
  113. django_cfg/core/integration/__init__.py +47 -0
  114. django_cfg/core/integration/commands_collector.py +239 -0
  115. django_cfg/core/integration/display/__init__.py +15 -0
  116. django_cfg/core/integration/display/base.py +157 -0
  117. django_cfg/core/integration/display/ngrok.py +164 -0
  118. django_cfg/core/integration/display/startup.py +815 -0
  119. django_cfg/core/integration/url_integration.py +123 -0
  120. django_cfg/core/integration/version_checker.py +160 -0
  121. django_cfg/management/commands/auto_generate.py +4 -0
  122. django_cfg/management/commands/check_settings.py +6 -0
  123. django_cfg/management/commands/clear_constance.py +5 -2
  124. django_cfg/management/commands/create_token.py +6 -0
  125. django_cfg/management/commands/list_urls.py +6 -0
  126. django_cfg/management/commands/migrate_all.py +6 -0
  127. django_cfg/management/commands/migrator.py +3 -0
  128. django_cfg/management/commands/rundramatiq.py +6 -0
  129. django_cfg/management/commands/runserver_ngrok.py +51 -29
  130. django_cfg/management/commands/script.py +6 -0
  131. django_cfg/management/commands/show_config.py +12 -2
  132. django_cfg/management/commands/show_urls.py +4 -0
  133. django_cfg/management/commands/superuser.py +6 -0
  134. django_cfg/management/commands/task_clear.py +4 -1
  135. django_cfg/management/commands/task_status.py +3 -1
  136. django_cfg/management/commands/test_email.py +3 -0
  137. django_cfg/management/commands/test_telegram.py +6 -0
  138. django_cfg/management/commands/test_twilio.py +6 -0
  139. django_cfg/management/commands/tree.py +6 -0
  140. django_cfg/management/commands/validate_config.py +155 -149
  141. django_cfg/models/constance.py +31 -11
  142. django_cfg/models/payments.py +175 -492
  143. django_cfg/modules/django_logger.py +160 -146
  144. django_cfg/modules/django_unfold/dashboard.py +64 -16
  145. django_cfg/registry/core.py +1 -0
  146. django_cfg/template_archive/django_sample.zip +0 -0
  147. django_cfg/utils/smart_defaults.py +222 -571
  148. django_cfg/utils/toolkit.py +51 -11
  149. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
  150. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
  151. django_cfg/apps/payments/__init__.py +0 -8
  152. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  153. django_cfg/apps/payments/config/module.py +0 -70
  154. django_cfg/apps/payments/config/providers.py +0 -105
  155. django_cfg/apps/payments/config/settings.py +0 -96
  156. django_cfg/apps/payments/config/utils.py +0 -52
  157. django_cfg/apps/payments/decorators.py +0 -291
  158. django_cfg/apps/payments/management/commands/README.md +0 -146
  159. django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
  160. django_cfg/apps/payments/managers/__init__.py +0 -23
  161. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  162. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  163. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  164. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  165. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  166. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  167. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  168. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  169. django_cfg/apps/payments/models/events.py +0 -73
  170. django_cfg/apps/payments/serializers/__init__.py +0 -57
  171. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  172. django_cfg/apps/payments/serializers/balance.py +0 -59
  173. django_cfg/apps/payments/serializers/currencies.py +0 -63
  174. django_cfg/apps/payments/serializers/payments.py +0 -62
  175. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  176. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  177. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  178. django_cfg/apps/payments/services/cache/base.py +0 -30
  179. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  180. django_cfg/apps/payments/services/internal_types.py +0 -461
  181. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  182. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  183. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  184. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  185. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  186. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  187. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  188. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  189. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  190. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  191. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  192. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  193. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  194. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  195. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  196. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  197. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  198. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  199. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  200. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  201. django_cfg/apps/payments/services/security/__init__.py +0 -34
  202. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  203. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  204. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  205. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  206. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  207. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  208. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  209. django_cfg/apps/payments/tasks/__init__.py +0 -12
  210. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  211. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  212. django_cfg/apps/payments/templates/payments/base.html +0 -182
  213. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  214. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  215. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  216. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  217. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  218. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  219. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  220. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  221. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  222. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  223. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  224. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  225. django_cfg/apps/payments/templates/payments/test.html +0 -213
  226. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  227. django_cfg/apps/payments/utils/__init__.py +0 -43
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -239
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -63
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -122
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  241. django_cfg/apps/payments/views/templates/base.py +0 -212
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -244
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -66
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/template_archive/.gitignore +0 -1
  252. django_cfg/template_archive/__init__.py +0 -0
  253. django_cfg/urls.py +0 -33
  254. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  255. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  256. {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
- Handles API key authentication and subscription validation.
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
- from django_cfg.modules.django_logger import get_logger
7
- from typing import Optional, Tuple
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 ..models import APIKey, Subscription, EndpointGroup
13
- from ..services import ApiKeyCache, RateLimitCache
14
- from ..services.security import error_handler, SecurityError
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
- logger = get_logger("api_access")
20
+ User = get_user_model()
21
+ logger = get_logger("api_access_middleware")
17
22
 
18
23
 
19
24
  class APIAccessMiddleware(MiddlewareMixin):
20
25
  """
21
- Middleware for API access control using API keys and subscriptions.
26
+ Enhanced API Access Control Middleware.
22
27
 
23
28
  Features:
24
- - API key validation
25
- - Subscription status checking
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
- # Paths that don't require API key authentication
36
- self.exempt_paths = getattr(settings, 'PAYMENTS_EXEMPT_PATHS', [
37
- '/api/v1/api-key/validate/',
38
- '/api/v1/api-key/create/',
39
- '/admin/',
40
- '/cfg/',
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
- # API prefixes that require authentication
44
- self.api_prefixes = getattr(settings, 'PAYMENTS_API_PREFIXES', [
45
- '/api/v1/',
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
- """Process incoming request for API access control."""
90
+ """
91
+ Process incoming request for API access control.
50
92
 
51
- # Skip non-API requests
52
- if not self._is_api_request(request):
93
+ Returns JsonResponse if access should be denied, None to continue.
94
+ """
95
+ if not self.enabled:
53
96
  return None
54
97
 
55
- # Skip exempt paths
56
- if self._is_exempt_path(request):
98
+ # Check if this path requires authentication
99
+ if not self._requires_authentication(request.path):
57
100
  return None
58
101
 
59
- # Extract API key
60
- api_key = self._extract_api_key(request)
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
- # Validate API key
78
- api_key_obj = self._validate_api_key(api_key)
79
- if not api_key_obj:
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
- return self._error_response(
95
- 'Invalid or expired API key',
96
- status=401,
97
- error_code='INVALID_API_KEY'
98
- )
99
-
100
- # Check subscription access
101
- endpoint_group = self._get_endpoint_group(request)
102
- if endpoint_group:
103
- subscription = self._check_subscription_access(api_key_obj.user, endpoint_group)
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
- # Check usage limits
112
- if self._is_usage_exceeded(subscription):
113
- return self._error_response(
114
- 'Usage limit exceeded for this subscription',
115
- status=429,
116
- error_code='USAGE_EXCEEDED'
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
- # Store subscription in request for usage tracking
120
- request.payment_subscription = subscription
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
- # Store API key in request
123
- request.payment_api_key = api_key_obj
124
- request.payment_user = api_key_obj.user
187
+ if not requires_auth:
188
+ return False
125
189
 
126
- return None
127
-
128
- def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
129
- """Process response to track API usage."""
190
+ # Check exempt paths
191
+ if path in self.exempt_paths:
192
+ return False
130
193
 
131
- # Track usage if API key was used
132
- if hasattr(request, 'payment_api_key') and hasattr(request, 'payment_subscription'):
133
- self._track_usage(request.payment_api_key, request.payment_subscription, request)
194
+ # Check exempt patterns
195
+ for pattern in self.exempt_patterns:
196
+ if pattern.match(path):
197
+ return False
134
198
 
135
- return response
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
- """Extract API key from request headers or query params."""
202
+ """
203
+ Extract API key from request headers or query parameters.
149
204
 
150
- # Try Authorization header first (Bearer token)
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 ' prefix
215
+ return auth_header[7:] # Remove 'Bearer '
216
+ elif auth_header.startswith('ApiKey '):
217
+ return auth_header[7:] # Remove 'ApiKey '
154
218
 
155
- # Try X-API-Key header
156
- api_key = request.META.get('HTTP_X_API_KEY')
157
- if api_key:
158
- return api_key
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
- # Try query parameter (less secure, for testing)
161
- api_key = request.GET.get('api_key')
162
- if api_key:
163
- return api_key
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, api_key: str) -> Optional[APIKey]:
168
- """Validate API key using Redis cache with DB fallback."""
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
- # Try Redis first
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
- # Fallback to database
177
- api_key_obj = APIKey.objects.select_related('user').filter(
178
- key_value=api_key,
179
- is_active=True
180
- ).first()
181
-
182
- if api_key_obj and api_key_obj.is_valid():
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
- # Update last used
187
- api_key_obj.record_usage()
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"Error validating API key: {e}")
195
- return None
196
-
197
- def _get_endpoint_group(self, request: HttpRequest) -> Optional[EndpointGroup]:
198
- """Determine endpoint group based on request path."""
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, user, endpoint_group: EndpointGroup) -> Optional[Subscription]:
223
- """Check if user has active subscription for endpoint group."""
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 subscription for this endpoint group
227
- subscription = Subscription.objects.filter(
228
- user=user,
229
- endpoint_group=endpoint_group,
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
- ).first()
303
+ ).select_related('tariff', 'endpoint_group')
233
304
 
234
- return subscription
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
- except Exception as e:
237
- logger.error(f"Error checking subscription access: {e}")
238
- return None
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 subscription.usage_current >= subscription.usage_limit
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"Error checking usage limits: {e}")
252
- return False
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 _track_usage(self, api_key: APIKey, subscription: Subscription, request: HttpRequest):
255
- """Track API usage for billing and analytics."""
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 subscription usage
259
- subscription.usage_current += 1
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 Redis cache
263
- self.redis_service.increment_usage(api_key.key_value, subscription.id)
343
+ # Update usage analytics in cache
344
+ today = timezone.now().date().isoformat()
264
345
 
265
- # Log usage for analytics
266
- logger.info(
267
- f"API usage tracked - User: {api_key.user.id}, "
268
- f"Subscription: {subscription.id}, "
269
- f"Path: {request.path}, "
270
- f"Usage: {subscription.usage_current}/{subscription.usage_limit}"
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.error(f"Error tracking usage: {e}")
355
+ logger.warning(f"Usage tracking failed: {e}")
275
356
 
276
357
  def _get_client_ip(self, request: HttpRequest) -> str:
277
- """Extract client IP address from request."""
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
- ip = x_forwarded_for.split(',')[0].strip()
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
- 'error': {
290
- 'code': error_code,
291
- 'message': message,
292
- 'timestamp': timezone.now().isoformat(),
293
- }
294
- }, status=status)
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