django-cfg 1.2.31__py3-none-any.whl â 1.3.3__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/cleanup_expired_data.py +419 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +297 -225
- 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/management/commands/process_pending_payments.py +357 -0
- django_cfg/apps/payments/management/commands/test_providers.py +434 -0
- 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 +153 -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_service/__init__.py +143 -0
- django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
- django_cfg/apps/payments/services/{cache/base.py â cache_service/interfaces.py} +3 -1
- django_cfg/apps/payments/services/cache_service/keys.py +49 -0
- django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
- django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
- 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 +371 -465
- 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 +227 -570
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info â django_cfg-1.3.3.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info â django_cfg-1.3.3.dist-info}/RECORD +162 -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/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/simple_cache.py +0 -135
- 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.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info â django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info â django_cfg-1.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,347 +1,631 @@
|
|
1
1
|
"""
|
2
|
-
Admin interface
|
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.
|
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,
|
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
|
-
"""
|
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
|
-
'
|
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'
|
56
|
+
list_display_links = ['key_display']
|
34
57
|
|
35
58
|
search_fields = [
|
36
59
|
'name',
|
37
60
|
'user__email',
|
38
|
-
'
|
39
|
-
'
|
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
|
-
'
|
70
|
+
'expires_at'
|
51
71
|
]
|
52
72
|
|
53
73
|
readonly_fields = [
|
54
|
-
'
|
55
|
-
'key_prefix',
|
56
|
-
'usage_count',
|
57
|
-
'last_used',
|
74
|
+
'key',
|
58
75
|
'created_at',
|
59
|
-
'
|
60
|
-
'
|
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': [
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
91
|
+
'fields': [
|
92
|
+
'user',
|
93
|
+
'name',
|
94
|
+
'key'
|
95
|
+
]
|
70
96
|
}),
|
71
|
-
('
|
72
|
-
'fields': [
|
97
|
+
('Status & Security', {
|
98
|
+
'fields': [
|
99
|
+
'is_active',
|
100
|
+
'expires_at'
|
101
|
+
]
|
73
102
|
}),
|
74
103
|
('Usage Statistics', {
|
75
|
-
'fields': [
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
89
|
-
|
90
|
-
'
|
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
|
103
|
-
|
104
|
-
|
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
|
-
|
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
|
-
'<
|
113
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
'<
|
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
|
-
|
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
|
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
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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 =
|
173
|
-
|
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
|
-
'<
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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="
|
190
|
-
def
|
191
|
-
"""Display
|
192
|
-
if obj.expires_at:
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
'<
|
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="
|
205
|
-
def
|
206
|
-
"""Display
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
214
|
-
|
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
|
218
|
-
'<
|
219
|
-
'
|
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
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
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
|
240
|
-
'<
|
241
|
-
'
|
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.
|
364
|
+
obj.created_at.strftime('%Y-%m-%d'),
|
365
|
+
naturaltime(obj.created_at)
|
249
366
|
)
|
250
367
|
|
251
|
-
|
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
|
-
#
|
434
|
+
# ===== ADMIN ACTIONS =====
|
254
435
|
|
255
|
-
@action(
|
256
|
-
|
257
|
-
""
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
api_key
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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="
|
278
|
-
icon="
|
473
|
+
description="đ
Extend Expiry (30 days)",
|
474
|
+
icon="schedule",
|
279
475
|
variant=ActionVariant.INFO
|
280
476
|
)
|
281
|
-
def
|
282
|
-
"""
|
283
|
-
|
284
|
-
|
285
|
-
|
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"
|
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="
|
294
|
-
icon="
|
497
|
+
description="đ Rotate Keys",
|
498
|
+
icon="refresh",
|
295
499
|
variant=ActionVariant.WARNING
|
296
500
|
)
|
297
|
-
def
|
298
|
-
"""
|
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
|
-
|
305
|
-
api_key.save()
|
504
|
+
rotated_count = 0
|
306
505
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
328
|
-
"
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
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
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
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"
|
344
|
-
level='info'
|
628
|
+
f"đ Exported usage report for {queryset.count()} API keys"
|
345
629
|
)
|
346
|
-
|
347
|
-
|
630
|
+
|
631
|
+
return response
|