django-cfg 1.2.29__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 (258) 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 -9
  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 +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  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 +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  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 +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  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 +207 -67
  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 -284
  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 +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  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 +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  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 +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  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 +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  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/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  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 -62
  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 -111
  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 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  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 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  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 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,347 +1,631 @@
1
1
  """
2
- Admin interface for API key management.
2
+ API Key Admin interface with Unfold integration.
3
+
4
+ Advanced API key management with security features and monitoring.
3
5
  """
4
6
 
5
7
  from django.contrib import admin
6
8
  from django.utils.html import format_html
7
9
  from django.contrib.humanize.templatetags.humanize import naturaltime
8
- from django.urls import reverse
10
+ from django.contrib import messages
9
11
  from django.shortcuts import redirect
12
+ from django.utils.safestring import mark_safe
13
+ from django.db.models import Count, Sum, Q
14
+ from django.utils import timezone
15
+ from datetime import timedelta
16
+ from typing import Optional
17
+
10
18
  from unfold.admin import ModelAdmin
11
19
  from unfold.decorators import display, action
12
20
  from unfold.enums import ActionVariant
13
21
 
14
22
  from ..models import APIKey
15
- from .filters import APIKeyStatusFilter, UserEmailFilter, RecentActivityFilter
23
+ from .filters import APIKeyStatusFilter, RecentActivityFilter
24
+ from django_cfg.modules.django_logger import get_logger
25
+
26
+ logger = get_logger("api_keys_admin")
16
27
 
17
28
 
18
29
  @admin.register(APIKey)
19
30
  class APIKeyAdmin(ModelAdmin):
20
- """Admin interface for API keys."""
31
+ """
32
+ Advanced API Key admin with security features and monitoring.
33
+
34
+ Features:
35
+ - Security-focused key management
36
+ - Usage monitoring and analytics
37
+ - Expiration management and alerts
38
+ - Bulk operations with audit trail
39
+ - Key rotation and deactivation
40
+ """
41
+
42
+ # Custom template for API key statistics
43
+ change_list_template = 'admin/payments/apikey/change_list.html'
21
44
 
22
45
  list_display = [
23
46
  'key_display',
24
47
  'user_display',
25
- 'name',
48
+ 'name_display',
26
49
  'status_display',
27
50
  'usage_display',
51
+ 'expiry_display',
28
52
  'last_used_display',
29
- 'expires_display',
30
53
  'created_at_display'
31
54
  ]
32
55
 
33
- list_display_links = ['key_display', 'name']
56
+ list_display_links = ['key_display']
34
57
 
35
58
  search_fields = [
36
59
  'name',
37
60
  'user__email',
38
- 'user__first_name',
39
- 'user__last_name',
40
- 'key_value',
41
- 'key_prefix'
61
+ 'user__username',
62
+ 'key' # Be careful with this in production
42
63
  ]
43
64
 
44
65
  list_filter = [
45
66
  APIKeyStatusFilter,
46
- UserEmailFilter,
47
67
  RecentActivityFilter,
48
68
  'is_active',
49
69
  'created_at',
50
- 'last_used'
70
+ 'expires_at'
51
71
  ]
52
72
 
53
73
  readonly_fields = [
54
- 'key_value',
55
- 'key_prefix',
56
- 'usage_count',
57
- 'last_used',
74
+ 'key',
58
75
  'created_at',
59
- 'key_statistics',
60
- 'usage_history'
76
+ 'updated_at',
77
+ 'last_used_at'
78
+ ]
79
+
80
+ # Unfold actions
81
+ actions_list = [
82
+ 'deactivate_keys',
83
+ 'extend_expiry',
84
+ 'rotate_keys',
85
+ 'send_expiry_alerts',
86
+ 'export_usage_report'
61
87
  ]
62
88
 
63
89
  fieldsets = [
64
90
  ('API Key Information', {
65
- 'fields': ['name', 'user']
66
- }),
67
- ('Key Details', {
68
- 'fields': ['key_value', 'key_prefix'],
69
- 'classes': ['collapse']
91
+ 'fields': [
92
+ 'user',
93
+ 'name',
94
+ 'key'
95
+ ]
70
96
  }),
71
- ('Settings', {
72
- 'fields': ['is_active', 'expires_at']
97
+ ('Status & Security', {
98
+ 'fields': [
99
+ 'is_active',
100
+ 'expires_at'
101
+ ]
73
102
  }),
74
103
  ('Usage Statistics', {
75
- 'fields': ['usage_count', 'last_used', 'key_statistics'],
76
- 'classes': ['collapse']
77
- }),
78
- ('Usage History', {
79
- 'fields': ['usage_history'],
80
- 'classes': ['collapse']
104
+ 'fields': [
105
+ 'total_requests',
106
+ 'last_used_at'
107
+ ]
81
108
  }),
82
109
  ('Timestamps', {
83
- 'fields': ['created_at'],
110
+ 'fields': ['created_at', 'updated_at'],
84
111
  'classes': ['collapse']
85
112
  })
86
113
  ]
87
114
 
88
- actions = [
89
- 'activate_keys',
90
- 'deactivate_keys',
91
- 'reset_usage'
92
- ]
93
-
94
- actions_detail = [
95
- 'regenerate_key',
96
- 'view_usage_stats',
97
- 'deactivate_key'
98
- ]
115
+ def get_queryset(self, request):
116
+ """Optimize queryset with user data."""
117
+ return super().get_queryset(request).select_related('user')
99
118
 
100
- @display(description="API Key")
119
+ @display(description="API Key", ordering='key')
101
120
  def key_display(self, obj):
102
- """Display API key with masking."""
103
- if obj.key_value:
104
- masked_key = f"{obj.key_prefix}***{obj.key_value[-4:]}"
121
+ """Display masked API key with copy functionality."""
122
+ # Show only first 8 and last 4 characters for security
123
+ masked_key = f"{obj.key[:8]}...{obj.key[-4:]}"
124
+
125
+ # Determine key status for styling
126
+ if not obj.is_active:
127
+ status_class = "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
128
+ status_icon = "🔴"
129
+ elif obj.expires_at and obj.expires_at <= timezone.now():
130
+ status_class = "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
131
+ status_icon = "⌛"
105
132
  else:
106
- masked_key = f"{obj.key_prefix}***"
107
-
108
- status_color = '#28a745' if obj.is_active else '#dc3545'
109
- status_icon = '🔑' if obj.is_active else '🔒'
133
+ status_class = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
134
+ status_icon = "đŸŸĸ"
110
135
 
111
136
  return format_html(
112
- '<span style="color: {};">{}</span> <code>{}</code>',
113
- status_color,
137
+ '<div class="flex items-center space-x-2">'
138
+ '<span class="text-sm">{}</span>'
139
+ '<span class="font-mono text-xs {} px-2 py-1 rounded" title="Click to copy full key">{}</span>'
140
+ '</div>',
114
141
  status_icon,
142
+ status_class,
115
143
  masked_key
116
144
  )
117
145
 
118
- @display(description="User")
146
+ @display(description="User", ordering='user__email')
119
147
  def user_display(self, obj):
120
- """Display user information."""
121
- user = obj.user
122
- if hasattr(user, 'avatar') and user.avatar:
123
- avatar_html = f'<img src="{user.avatar.url}" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 6px;" />'
124
- else:
125
- initials = f"{user.first_name[:1]}{user.last_name[:1]}" if user.first_name and user.last_name else user.email[:2]
126
- avatar_html = f'<div style="width: 20px; height: 20px; border-radius: 50%; background: #6c757d; color: white; display: inline-flex; align-items: center; justify-content: center; font-size: 9px; margin-right: 6px;">{initials.upper()}</div>'
127
-
128
- return format_html(
129
- '{}<strong>{}</strong><br><small>{}</small>',
130
- avatar_html,
131
- user.get_full_name() or user.email,
132
- user.email
133
- )
134
-
135
- @display(description="Status")
136
- def status_display(self, obj):
137
- """Display status with validation check."""
138
- if not obj.is_active:
148
+ """Display user information with subscription status."""
149
+ if obj.user:
150
+ # Check if user has active subscription
151
+ from ..models import Subscription
152
+
153
+ active_subscription = Subscription.objects.filter(
154
+ user=obj.user,
155
+ status=Subscription.SubscriptionStatus.ACTIVE
156
+ ).first()
157
+
158
+ subscription_info = ""
159
+ if active_subscription:
160
+ subscription_info = format_html(
161
+ '<div class="text-xs text-blue-600 dark:text-blue-400">{} tier</div>',
162
+ active_subscription.tariff.tier.title()
163
+ )
164
+ else:
165
+ subscription_info = format_html(
166
+ '<div class="text-xs text-gray-500">No active subscription</div>'
167
+ )
168
+
139
169
  return format_html(
140
- '<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
170
+ '<div>'
171
+ '<div class="font-medium text-gray-900 dark:text-gray-100">{}</div>'
172
+ '<div class="text-xs text-gray-500">{}</div>'
173
+ '{}'
174
+ '</div>',
175
+ obj.user.get_full_name() or obj.user.username,
176
+ obj.user.email,
177
+ subscription_info
141
178
  )
142
-
143
- if obj.expires_at and obj.expires_at <= obj.__class__.objects.model._get_current_time():
179
+ return format_html('<span class="text-gray-500">No user</span>')
180
+
181
+ @display(description="Name", ordering='name')
182
+ def name_display(self, obj):
183
+ """Display API key name with truncation."""
184
+ if len(obj.name) > 30:
144
185
  return format_html(
145
- '<span style="background: #ffc107; color: black; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Expired</span>'
186
+ '<span title="{}">{}</span>',
187
+ obj.name,
188
+ obj.name[:27] + "..."
146
189
  )
190
+ return obj.name
191
+
192
+ @display(description="Status")
193
+ def status_display(self, obj):
194
+ """Display comprehensive status with multiple indicators."""
195
+ badges = []
147
196
 
148
- if obj.is_valid():
149
- return format_html(
150
- '<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
151
- )
197
+ # Active/Inactive status
198
+ if obj.is_active:
199
+ badges.append('<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">✅ Active</span>')
152
200
  else:
153
- return format_html(
154
- '<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Invalid</span>'
155
- )
201
+ badges.append('<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">❌ Inactive</span>')
202
+
203
+ # Expiry status
204
+ if obj.expires_at:
205
+ now = timezone.now()
206
+ if obj.expires_at <= now:
207
+ badges.append('<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">⌛ Expired</span>')
208
+ elif obj.expires_at <= now + timedelta(days=7):
209
+ badges.append('<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900 dark:text-orange-200">âš ī¸ Expiring Soon</span>')
210
+
211
+ # Usage status
212
+ if obj.total_requests == 0:
213
+ badges.append('<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">🆕 Unused</span>')
214
+ elif obj.total_requests >= 10000:
215
+ badges.append('<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-200">đŸ”Ĩ Heavy Use</span>')
216
+
217
+ return format_html('<div class="space-y-1">{}</div>', ''.join(badges))
156
218
 
157
219
  @display(description="Usage")
158
220
  def usage_display(self, obj):
159
- """Display usage statistics."""
160
- usage_count = obj.usage_count
161
-
162
- if usage_count == 0:
163
- color = '#6c757d'
164
- text = 'Never used'
165
- elif usage_count < 100:
166
- color = '#28a745'
167
- text = f'{usage_count} calls'
168
- elif usage_count < 1000:
169
- color = '#ffc107'
170
- text = f'{usage_count} calls'
221
+ """Display usage statistics with visual indicators."""
222
+ total_requests = obj.total_requests
223
+
224
+ # Determine usage level and color
225
+ if total_requests == 0:
226
+ color = "text-gray-600 dark:text-gray-400"
227
+ icon = "🆕"
228
+ level = "Unused"
229
+ elif total_requests < 100:
230
+ color = "text-green-600 dark:text-green-400"
231
+ icon = "đŸŸĸ"
232
+ level = "Light"
233
+ elif total_requests < 1000:
234
+ color = "text-yellow-600 dark:text-yellow-400"
235
+ icon = "🟡"
236
+ level = "Moderate"
237
+ elif total_requests < 10000:
238
+ color = "text-orange-600 dark:text-orange-400"
239
+ icon = "🟠"
240
+ level = "Heavy"
171
241
  else:
172
- color = '#dc3545'
173
- text = f'{usage_count:,} calls'
242
+ color = "text-red-600 dark:text-red-400"
243
+ icon = "🔴"
244
+ level = "Extreme"
245
+
246
+ # Calculate recent usage (last 7 days)
247
+ recent_threshold = timezone.now() - timedelta(days=7)
248
+ # Note: This would require additional tracking in production
249
+ # For now, we'll show total usage
174
250
 
175
251
  return format_html(
176
- '<span style="color: {}; font-weight: bold;">{}</span>',
177
- color, text
178
- )
179
-
180
- @display(description="Last Used")
181
- def last_used_display(self, obj):
182
- """Display last used time."""
183
- if obj.last_used:
184
- return naturaltime(obj.last_used)
185
- return format_html(
186
- '<span style="color: #6c757d;">Never</span>'
252
+ '<div class="text-center">'
253
+ '<div class="font-bold {} text-lg">'
254
+ '<span class="mr-1">{}</span>{:,}'
255
+ '</div>'
256
+ '<div class="text-xs text-gray-500">{} usage</div>'
257
+ '</div>',
258
+ color,
259
+ icon,
260
+ total_requests,
261
+ level
187
262
  )
188
263
 
189
- @display(description="Expires")
190
- def expires_display(self, obj):
191
- """Display expiration time."""
192
- if obj.expires_at:
193
- from django.utils import timezone
194
- if obj.expires_at <= timezone.now():
195
- return format_html(
196
- '<span style="color: #dc3545;">Expired</span>'
197
- )
198
- else:
199
- return naturaltime(obj.expires_at)
264
+ @display(description="Expiry", ordering='expires_at')
265
+ def expiry_display(self, obj):
266
+ """Display expiry information with countdown."""
267
+ if not obj.expires_at:
268
+ return format_html(
269
+ '<div class="text-center text-blue-600 dark:text-blue-400">'
270
+ '<div class="font-bold">∞</div>'
271
+ '<div class="text-xs">Never expires</div>'
272
+ '</div>'
273
+ )
274
+
275
+ now = timezone.now()
276
+
277
+ if obj.expires_at <= now:
278
+ # Already expired
279
+ return format_html(
280
+ '<div class="text-center text-red-600 dark:text-red-400">'
281
+ '<div class="font-bold">Expired</div>'
282
+ '<div class="text-xs">{}</div>'
283
+ '</div>',
284
+ naturaltime(obj.expires_at)
285
+ )
286
+
287
+ time_remaining = obj.expires_at - now
288
+
289
+ if time_remaining < timedelta(hours=24):
290
+ color = "text-red-600 dark:text-red-400"
291
+ icon = "🚨"
292
+ elif time_remaining < timedelta(days=7):
293
+ color = "text-orange-600 dark:text-orange-400"
294
+ icon = "âš ī¸"
295
+ else:
296
+ color = "text-green-600 dark:text-green-400"
297
+ icon = "✅"
298
+
200
299
  return format_html(
201
- '<span style="color: #6c757d;">Never</span>'
300
+ '<div class="text-center {}">'
301
+ '<div><span class="mr-1">{}</span>{}</div>'
302
+ '<div class="text-xs">{}</div>'
303
+ '</div>',
304
+ color,
305
+ icon,
306
+ naturaltime(obj.expires_at),
307
+ obj.expires_at.strftime('%Y-%m-%d')
202
308
  )
203
309
 
204
- @display(description="Created")
205
- def created_at_display(self, obj):
206
- """Display creation date."""
207
- return naturaltime(obj.created_at)
208
-
209
- def key_statistics(self, obj):
210
- """Show API key statistics."""
211
- from django.utils import timezone
310
+ @display(description="Last Used", ordering='last_used_at')
311
+ def last_used_display(self, obj):
312
+ """Display last usage with recency indicators."""
313
+ if not obj.last_used_at:
314
+ return format_html(
315
+ '<div class="text-center text-gray-500">'
316
+ '<div>Never</div>'
317
+ '<div class="text-xs">🆕 Unused</div>'
318
+ '</div>'
319
+ )
320
+
321
+ now = timezone.now()
322
+ time_since_use = now - obj.last_used_at
212
323
 
213
- # Calculate usage trends
214
- recent_usage = 0 # In real implementation, calculate from usage logs
324
+ if time_since_use < timedelta(minutes=5):
325
+ color = "text-green-600 dark:text-green-400"
326
+ icon = "đŸŸĸ"
327
+ status = "Just now"
328
+ elif time_since_use < timedelta(hours=1):
329
+ color = "text-green-600 dark:text-green-400"
330
+ icon = "đŸŸĸ"
331
+ status = "Recently"
332
+ elif time_since_use < timedelta(days=1):
333
+ color = "text-yellow-600 dark:text-yellow-400"
334
+ icon = "🟡"
335
+ status = "Today"
336
+ elif time_since_use < timedelta(days=7):
337
+ color = "text-orange-600 dark:text-orange-400"
338
+ icon = "🟠"
339
+ status = "This week"
340
+ else:
341
+ color = "text-red-600 dark:text-red-400"
342
+ icon = "🔴"
343
+ status = "Inactive"
215
344
 
216
345
  return format_html(
217
- '<div style="line-height: 1.6;">'
218
- '<strong>API Key Statistics:</strong><br>'
219
- 'â€ĸ Total Usage: <strong>{:,}</strong> calls<br>'
220
- 'â€ĸ Status: {}<br>'
221
- 'â€ĸ Valid: {}<br>'
222
- 'â€ĸ Last Used: {}<br>'
223
- 'â€ĸ Expires: {}<br>'
224
- 'â€ĸ Created: {}<br>'
346
+ '<div class="text-center {}">'
347
+ '<div><span class="mr-1">{}</span>{}</div>'
348
+ '<div class="text-xs">{}</div>'
225
349
  '</div>',
226
- obj.usage_count,
227
- 'Active' if obj.is_active else 'Inactive',
228
- 'Yes' if obj.is_valid() else 'No',
229
- naturaltime(obj.last_used) if obj.last_used else 'Never',
230
- naturaltime(obj.expires_at) if obj.expires_at else 'Never',
231
- naturaltime(obj.created_at)
350
+ color,
351
+ icon,
352
+ naturaltime(obj.last_used_at),
353
+ status
232
354
  )
233
355
 
234
- key_statistics.short_description = "Key Statistics"
235
-
236
- def usage_history(self, obj):
237
- """Show usage history (placeholder for future implementation)."""
356
+ @display(description="Created", ordering='created_at')
357
+ def created_at_display(self, obj):
358
+ """Display creation date."""
238
359
  return format_html(
239
- '<div style="line-height: 1.6;">'
240
- '<strong>Usage History:</strong><br>'
241
- 'â€ĸ Total API Calls: {:,}<br>'
242
- 'â€ĸ Last 24h: N/A<br>'
243
- 'â€ĸ Last 7 days: N/A<br>'
244
- 'â€ĸ Last 30 days: N/A<br>'
245
- '<br>'
246
- '<em>Detailed usage tracking will be implemented with analytics service.</em>'
360
+ '<div class="text-xs">'
361
+ '<div>{}</div>'
362
+ '<div class="text-gray-500">{}</div>'
247
363
  '</div>',
248
- obj.usage_count
364
+ obj.created_at.strftime('%Y-%m-%d'),
365
+ naturaltime(obj.created_at)
249
366
  )
250
367
 
251
- usage_history.short_description = "Usage History"
368
+ def changelist_view(self, request, extra_context=None):
369
+ """Add API key statistics to changelist context."""
370
+ extra_context = extra_context or {}
371
+
372
+ try:
373
+ # Basic statistics
374
+ total_keys = APIKey.objects.count()
375
+ active_keys = APIKey.objects.filter(is_active=True).count()
376
+
377
+ # Expiry statistics
378
+ now = timezone.now()
379
+ expired_keys = APIKey.objects.filter(expires_at__lte=now).count()
380
+ expiring_soon = APIKey.objects.filter(
381
+ expires_at__lte=now + timedelta(days=7),
382
+ expires_at__gt=now
383
+ ).count()
384
+
385
+ # Usage statistics
386
+ total_requests = APIKey.objects.aggregate(
387
+ total=Sum('total_requests')
388
+ )['total'] or 0
389
+
390
+ unused_keys = APIKey.objects.filter(total_requests=0).count()
391
+ heavy_usage_keys = APIKey.objects.filter(total_requests__gte=10000).count()
392
+
393
+ # Recent activity
394
+ recent_threshold = timezone.now() - timedelta(days=7)
395
+ recently_used = APIKey.objects.filter(
396
+ last_used_at__gte=recent_threshold
397
+ ).count()
398
+
399
+ # Security alerts
400
+ never_used_old_keys = APIKey.objects.filter(
401
+ total_requests=0,
402
+ created_at__lte=timezone.now() - timedelta(days=30)
403
+ ).count()
404
+
405
+ # Top users by API key count
406
+ top_users = APIKey.objects.values(
407
+ 'user__email', 'user__username'
408
+ ).annotate(
409
+ key_count=Count('id'),
410
+ total_usage=Sum('total_requests')
411
+ ).order_by('-key_count')[:5]
412
+
413
+ extra_context.update({
414
+ 'api_key_stats': {
415
+ 'total_keys': total_keys,
416
+ 'active_keys': active_keys,
417
+ 'expired_keys': expired_keys,
418
+ 'expiring_soon': expiring_soon,
419
+ 'total_requests': total_requests,
420
+ 'unused_keys': unused_keys,
421
+ 'heavy_usage_keys': heavy_usage_keys,
422
+ 'recently_used': recently_used,
423
+ 'never_used_old_keys': never_used_old_keys,
424
+ 'top_users': top_users,
425
+ }
426
+ })
427
+
428
+ except Exception as e:
429
+ logger.warning(f"Failed to generate API key statistics: {e}")
430
+ extra_context['api_key_stats'] = None
431
+
432
+ return super().changelist_view(request, extra_context)
252
433
 
253
- # Admin Actions
434
+ # ===== ADMIN ACTIONS =====
254
435
 
255
- @action(description="🔑 Regenerate API Key")
256
- def regenerate_key(self, request, object_id):
257
- """Regenerate API key."""
258
- api_key = self.get_object(request, object_id)
259
- if not api_key:
260
- self.message_user(request, "API key not found.", level='error')
261
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
262
-
263
- # Generate new key
264
- import secrets
265
- api_key.key_value = f"ak_{secrets.token_urlsafe(32)}"
266
- api_key.usage_count = 0 # Reset usage
267
- api_key.save()
268
-
269
- self.message_user(
270
- request,
271
- f"API key '{api_key.name}' has been regenerated. Usage count reset to 0.",
272
- level='success'
273
- )
274
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
436
+ @action(
437
+ description="🔒 Deactivate Keys",
438
+ icon="block",
439
+ variant=ActionVariant.WARNING
440
+ )
441
+ def deactivate_keys(self, request, queryset):
442
+ """Deactivate selected API keys."""
443
+
444
+ active_keys = queryset.filter(is_active=True)
445
+ deactivated_count = 0
446
+
447
+ for api_key in active_keys:
448
+ try:
449
+ api_key.deactivate(reason=f"Deactivated by admin {request.user.username}")
450
+ deactivated_count += 1
451
+
452
+ except Exception as e:
453
+ logger.error(f"Failed to deactivate API key {api_key.id}: {e}")
454
+
455
+ if deactivated_count > 0:
456
+ messages.success(
457
+ request,
458
+ f"🔒 Deactivated {deactivated_count} API keys"
459
+ )
460
+ messages.info(
461
+ request,
462
+ "â„šī¸ Deactivated keys can be reactivated if needed"
463
+ )
464
+
465
+ skipped = queryset.count() - deactivated_count
466
+ if skipped > 0:
467
+ messages.info(
468
+ request,
469
+ f"â„šī¸ Skipped {skipped} keys (already inactive)"
470
+ )
275
471
 
276
472
  @action(
277
- description="📊 View Usage Statistics",
278
- icon="analytics",
473
+ description="📅 Extend Expiry (30 days)",
474
+ icon="schedule",
279
475
  variant=ActionVariant.INFO
280
476
  )
281
- def view_usage_stats(self, request, object_id):
282
- """View detailed usage statistics."""
283
- api_key = self.get_object(request, object_id)
284
- if api_key:
285
- self.message_user(
477
+ def extend_expiry(self, request, queryset):
478
+ """Extend expiry of selected API keys by 30 days."""
479
+
480
+ extended_count = 0
481
+
482
+ for api_key in queryset:
483
+ try:
484
+ api_key.extend_expiry(days=30)
485
+ extended_count += 1
486
+
487
+ except Exception as e:
488
+ logger.error(f"Failed to extend API key {api_key.id} expiry: {e}")
489
+
490
+ if extended_count > 0:
491
+ messages.success(
286
492
  request,
287
- f"Usage statistics view for '{api_key.name}' would open here.",
288
- level='info'
493
+ f"📅 Extended expiry for {extended_count} API keys by 30 days"
289
494
  )
290
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
291
495
 
292
496
  @action(
293
- description="🔒 Deactivate Key",
294
- icon="block",
497
+ description="🔄 Rotate Keys",
498
+ icon="refresh",
295
499
  variant=ActionVariant.WARNING
296
500
  )
297
- def deactivate_key(self, request, object_id):
298
- """Deactivate API key."""
299
- api_key = self.get_object(request, object_id)
300
- if not api_key:
301
- self.message_user(request, "API key not found.", level='error')
302
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
501
+ def rotate_keys(self, request, queryset):
502
+ """Rotate selected API keys (generate new keys)."""
303
503
 
304
- api_key.is_active = False
305
- api_key.save()
504
+ rotated_count = 0
306
505
 
307
- self.message_user(
308
- request,
309
- f"API key '{api_key.name}' has been deactivated.",
310
- level='warning'
311
- )
312
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
313
-
314
- # Bulk Actions
315
-
316
- def activate_keys(self, request, queryset):
317
- """Activate selected API keys."""
318
- count = queryset.update(is_active=True)
319
- self.message_user(
320
- request,
321
- f"Successfully activated {count} API keys.",
322
- level='success'
323
- )
324
-
325
- activate_keys.short_description = "🔓 Activate selected API keys"
506
+ for api_key in queryset:
507
+ try:
508
+ # Generate new key
509
+ old_key = api_key.key
510
+ api_key.generate_key()
511
+ api_key.save()
512
+
513
+ rotated_count += 1
514
+
515
+ logger.info(
516
+ f"API key rotated for user {api_key.user.email}",
517
+ extra={
518
+ 'api_key_id': str(api_key.id),
519
+ 'user_id': api_key.user.id,
520
+ 'old_key_prefix': old_key[:8],
521
+ 'new_key_prefix': api_key.key[:8],
522
+ 'rotated_by': request.user.username
523
+ }
524
+ )
525
+
526
+ except Exception as e:
527
+ logger.error(f"Failed to rotate API key {api_key.id}: {e}")
528
+
529
+ if rotated_count > 0:
530
+ messages.success(
531
+ request,
532
+ f"🔄 Rotated {rotated_count} API keys"
533
+ )
534
+ messages.warning(
535
+ request,
536
+ "âš ī¸ Users will need to update their applications with new keys!"
537
+ )
326
538
 
327
- def deactivate_keys(self, request, queryset):
328
- """Deactivate selected API keys."""
329
- count = queryset.update(is_active=False)
330
- self.message_user(
331
- request,
332
- f"Successfully deactivated {count} API keys.",
333
- level='warning'
539
+ @action(
540
+ description="🔔 Send Expiry Alerts",
541
+ icon="notifications",
542
+ variant=ActionVariant.INFO
543
+ )
544
+ def send_expiry_alerts(self, request, queryset):
545
+ """Send expiry alerts for keys expiring soon."""
546
+
547
+ now = timezone.now()
548
+ expiring_keys = queryset.filter(
549
+ is_active=True,
550
+ expires_at__lte=now + timedelta(days=7),
551
+ expires_at__gt=now
334
552
  )
553
+
554
+ alert_count = 0
555
+
556
+ for api_key in expiring_keys:
557
+ try:
558
+ # In production, this would send an actual notification
559
+ logger.info(
560
+ f"Expiry alert for API key {api_key.name}",
561
+ extra={
562
+ 'api_key_id': str(api_key.id),
563
+ 'user_email': api_key.user.email,
564
+ 'expires_at': api_key.expires_at.isoformat()
565
+ }
566
+ )
567
+ alert_count += 1
568
+
569
+ except Exception as e:
570
+ logger.error(f"Failed to send alert for API key {api_key.id}: {e}")
571
+
572
+ if alert_count > 0:
573
+ messages.success(
574
+ request,
575
+ f"🔔 Sent expiry alerts for {alert_count} API keys"
576
+ )
577
+ else:
578
+ messages.info(
579
+ request,
580
+ "â„šī¸ No API keys expiring soon in selection"
581
+ )
335
582
 
336
- deactivate_keys.short_description = "🔒 Deactivate selected API keys"
337
-
338
- def reset_usage(self, request, queryset):
339
- """Reset usage count for selected API keys."""
340
- count = queryset.update(usage_count=0)
341
- self.message_user(
583
+ @action(
584
+ description="📊 Export Usage Report",
585
+ icon="download",
586
+ variant=ActionVariant.INFO
587
+ )
588
+ def export_usage_report(self, request, queryset):
589
+ """Export API key usage report to CSV."""
590
+
591
+ import csv
592
+ from django.http import HttpResponse
593
+
594
+ response = HttpResponse(content_type='text/csv')
595
+ response['Content-Disposition'] = f'attachment; filename="api_keys_usage_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"'
596
+
597
+ writer = csv.writer(response)
598
+ writer.writerow([
599
+ 'Key Name', 'User Email', 'User Name', 'Total Requests', 'Is Active',
600
+ 'Created', 'Last Used', 'Expires', 'Status'
601
+ ])
602
+
603
+ for api_key in queryset:
604
+ # Determine status
605
+ if not api_key.is_active:
606
+ status = 'Inactive'
607
+ elif api_key.expires_at and api_key.expires_at <= timezone.now():
608
+ status = 'Expired'
609
+ elif api_key.total_requests == 0:
610
+ status = 'Unused'
611
+ else:
612
+ status = 'Active'
613
+
614
+ writer.writerow([
615
+ api_key.name,
616
+ api_key.user.email if api_key.user else '',
617
+ api_key.user.get_full_name() if api_key.user else '',
618
+ api_key.total_requests,
619
+ 'Yes' if api_key.is_active else 'No',
620
+ api_key.created_at.isoformat(),
621
+ api_key.last_used_at.isoformat() if api_key.last_used_at else '',
622
+ api_key.expires_at.isoformat() if api_key.expires_at else 'Never',
623
+ status
624
+ ])
625
+
626
+ messages.success(
342
627
  request,
343
- f"Successfully reset usage count for {count} API keys.",
344
- level='info'
628
+ f"📊 Exported usage report for {queryset.count()} API keys"
345
629
  )
346
-
347
- reset_usage.short_description = "🔄 Reset usage count"
630
+
631
+ return response