django-cfg 1.3.5__py3-none-any.whl â 1.3.9__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/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/{nowpayments.py â nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/apps/urls.py +1 -2
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +3 -0
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.5.dist-info â django_cfg-1.3.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.5.dist-info â django_cfg-1.3.9.dist-info}/RECORD +224 -118
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold â django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold â django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold â django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold â django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.5.dist-info â django_cfg-1.3.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.5.dist-info â django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.5.dist-info â django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,631 +1,264 @@
|
|
1
1
|
"""
|
2
|
-
API
|
2
|
+
API Keys Admin interface using Django Admin Utilities.
|
3
3
|
|
4
|
-
|
4
|
+
Clean API key management with security features.
|
5
5
|
"""
|
6
6
|
|
7
7
|
from django.contrib import admin
|
8
|
-
from django.
|
9
|
-
from django.contrib.humanize.templatetags.humanize import naturaltime
|
10
|
-
from django.contrib import messages
|
11
|
-
from django.shortcuts import redirect
|
12
|
-
from django.utils.safestring import mark_safe
|
13
|
-
from django.db.models import Count, Sum, Q
|
8
|
+
from django.db.models import Count, Q
|
14
9
|
from django.utils import timezone
|
15
10
|
from datetime import timedelta
|
16
|
-
from typing import Optional
|
17
11
|
|
18
12
|
from unfold.admin import ModelAdmin
|
19
|
-
from unfold.decorators import display, action
|
20
|
-
from unfold.enums import ActionVariant
|
21
13
|
|
22
|
-
from
|
23
|
-
|
14
|
+
from django_cfg.modules.django_admin import (
|
15
|
+
OptimizedModelAdmin,
|
16
|
+
DisplayMixin,
|
17
|
+
UserDisplayConfig,
|
18
|
+
StatusBadgeConfig,
|
19
|
+
DateTimeDisplayConfig,
|
20
|
+
Icons,
|
21
|
+
display,
|
22
|
+
action,
|
23
|
+
ActionVariant
|
24
|
+
)
|
25
|
+
from django_cfg.modules.django_admin.utils.badges import StatusBadge
|
24
26
|
from django_cfg.modules.django_logger import get_logger
|
25
27
|
|
28
|
+
from ..models import APIKey
|
29
|
+
|
26
30
|
logger = get_logger("api_keys_admin")
|
27
31
|
|
28
32
|
|
29
33
|
@admin.register(APIKey)
|
30
|
-
class APIKeyAdmin(ModelAdmin):
|
34
|
+
class APIKeyAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin):
|
31
35
|
"""
|
32
|
-
|
36
|
+
APIKey admin using Django Admin Utilities.
|
33
37
|
|
34
38
|
Features:
|
35
|
-
-
|
36
|
-
- Usage
|
37
|
-
-
|
38
|
-
-
|
39
|
-
- Key rotation and deactivation
|
39
|
+
- Secure API key display
|
40
|
+
- Usage tracking
|
41
|
+
- Activity monitoring
|
42
|
+
- Clean utilities integration
|
40
43
|
"""
|
41
44
|
|
42
|
-
#
|
43
|
-
|
45
|
+
# Performance optimization
|
46
|
+
select_related_fields = ['user']
|
47
|
+
annotations = {}
|
48
|
+
# Note: Annotations disabled until proper usage tracking is implemented
|
49
|
+
# 'usage_count': Count('usage_logs') or similar
|
44
50
|
|
51
|
+
# List configuration
|
45
52
|
list_display = [
|
46
53
|
'key_display',
|
47
54
|
'user_display',
|
48
55
|
'name_display',
|
49
56
|
'status_display',
|
50
57
|
'usage_display',
|
51
|
-
'
|
52
|
-
'last_used_display',
|
53
|
-
'created_at_display'
|
58
|
+
'created_display'
|
54
59
|
]
|
55
60
|
|
56
|
-
|
61
|
+
list_filter = [
|
62
|
+
'is_active',
|
63
|
+
'created_at',
|
64
|
+
'last_used_at'
|
65
|
+
]
|
57
66
|
|
58
67
|
search_fields = [
|
59
68
|
'name',
|
60
|
-
'user__email',
|
61
69
|
'user__username',
|
70
|
+
'user__email',
|
62
71
|
'key' # Be careful with this in production
|
63
72
|
]
|
64
73
|
|
65
|
-
list_filter = [
|
66
|
-
APIKeyStatusFilter,
|
67
|
-
RecentActivityFilter,
|
68
|
-
'is_active',
|
69
|
-
'created_at',
|
70
|
-
'expires_at'
|
71
|
-
]
|
72
|
-
|
73
74
|
readonly_fields = [
|
74
75
|
'key',
|
75
76
|
'created_at',
|
76
77
|
'updated_at',
|
77
|
-
'last_used_at'
|
78
|
+
'last_used_at',
|
79
|
+
'key_details_display'
|
78
80
|
]
|
79
81
|
|
80
|
-
#
|
81
|
-
|
82
|
-
'deactivate_keys',
|
83
|
-
'extend_expiry',
|
84
|
-
'rotate_keys',
|
85
|
-
'send_expiry_alerts',
|
86
|
-
'export_usage_report'
|
87
|
-
]
|
82
|
+
# Register actions
|
83
|
+
actions = ['activate_selected_keys', 'deactivate_selected_keys', 'regenerate_selected_keys']
|
88
84
|
|
89
|
-
|
90
|
-
|
91
|
-
'fields': [
|
92
|
-
'user',
|
93
|
-
'name',
|
94
|
-
'key'
|
95
|
-
]
|
96
|
-
}),
|
97
|
-
('Status & Security', {
|
98
|
-
'fields': [
|
99
|
-
'is_active',
|
100
|
-
'expires_at'
|
101
|
-
]
|
102
|
-
}),
|
103
|
-
('Usage Statistics', {
|
104
|
-
'fields': [
|
105
|
-
'total_requests',
|
106
|
-
'last_used_at'
|
107
|
-
]
|
108
|
-
}),
|
109
|
-
('Timestamps', {
|
110
|
-
'fields': ['created_at', 'updated_at'],
|
111
|
-
'classes': ['collapse']
|
112
|
-
})
|
113
|
-
]
|
114
|
-
|
115
|
-
def get_queryset(self, request):
|
116
|
-
"""Optimize queryset with user data."""
|
117
|
-
return super().get_queryset(request).select_related('user')
|
118
|
-
|
119
|
-
@display(description="API Key", ordering='key')
|
85
|
+
# Display methods using utilities
|
86
|
+
@display(description="API Key")
|
120
87
|
def key_display(self, obj):
|
121
|
-
"""
|
122
|
-
|
123
|
-
|
88
|
+
"""Masked API key display for security with key icon."""
|
89
|
+
if not obj.key:
|
90
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.KEY_OFF)
|
91
|
+
return StatusBadge.create(text="No Key", variant="secondary", config=config)
|
124
92
|
|
125
|
-
#
|
126
|
-
|
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 = "â"
|
132
|
-
else:
|
133
|
-
status_class = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
134
|
-
status_icon = "đĸ"
|
93
|
+
# Show first 8 and last 4 characters
|
94
|
+
masked_key = f"{obj.key[:8]}...{obj.key[-4:]}"
|
135
95
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
status_icon,
|
142
|
-
status_class,
|
143
|
-
masked_key
|
96
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.KEY)
|
97
|
+
return StatusBadge.create(
|
98
|
+
text=masked_key,
|
99
|
+
variant="info",
|
100
|
+
config=config
|
144
101
|
)
|
145
102
|
|
146
|
-
@display(description="User",
|
103
|
+
@display(description="User", header=True)
|
147
104
|
def user_display(self, obj):
|
148
|
-
"""
|
149
|
-
|
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
|
-
|
169
|
-
return format_html(
|
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
|
178
|
-
)
|
179
|
-
return format_html('<span class="text-gray-500">No user</span>')
|
105
|
+
"""User display with avatar."""
|
106
|
+
return self.display_user_with_avatar(obj, 'user')
|
180
107
|
|
181
|
-
@display(description="Name"
|
108
|
+
@display(description="Name")
|
182
109
|
def name_display(self, obj):
|
183
|
-
"""
|
184
|
-
if
|
185
|
-
return
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
110
|
+
"""API key name display."""
|
111
|
+
if not obj.name:
|
112
|
+
return StatusBadge.create(text="Unnamed", variant="secondary")
|
113
|
+
|
114
|
+
return StatusBadge.create(
|
115
|
+
text=obj.name,
|
116
|
+
variant="primary"
|
117
|
+
)
|
191
118
|
|
192
|
-
@display(description="Status")
|
119
|
+
@display(description="Status", label=True)
|
193
120
|
def status_display(self, obj):
|
194
|
-
"""
|
195
|
-
|
121
|
+
"""Status display with activity level."""
|
122
|
+
if not obj.is_active:
|
123
|
+
return self.display_status_auto(
|
124
|
+
type('obj', (), {'status': 'Inactive'})(),
|
125
|
+
'status',
|
126
|
+
StatusBadgeConfig(custom_mappings={'Inactive': 'danger'})
|
127
|
+
)
|
196
128
|
|
197
|
-
#
|
198
|
-
if obj.
|
199
|
-
|
129
|
+
# Determine activity level based on last usage
|
130
|
+
if not obj.last_used_at:
|
131
|
+
status = "Active (Unused)"
|
132
|
+
variant = "info"
|
200
133
|
else:
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
134
|
+
days_since_use = (timezone.now() - obj.last_used_at).days
|
135
|
+
|
136
|
+
if days_since_use <= 1:
|
137
|
+
status = "Active (Recent)"
|
138
|
+
variant = "success"
|
139
|
+
elif days_since_use <= 7:
|
140
|
+
status = "Active (This Week)"
|
141
|
+
variant = "success"
|
142
|
+
elif days_since_use <= 30:
|
143
|
+
status = "Active (This Month)"
|
144
|
+
variant = "warning"
|
145
|
+
else:
|
146
|
+
status = "Active (Idle)"
|
147
|
+
variant = "secondary"
|
210
148
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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>')
|
149
|
+
config = StatusBadgeConfig(
|
150
|
+
custom_mappings={status: variant},
|
151
|
+
show_icons=True
|
152
|
+
)
|
216
153
|
|
217
|
-
return
|
154
|
+
return self.display_status_auto(
|
155
|
+
type('obj', (), {'status': status})(),
|
156
|
+
'status',
|
157
|
+
config
|
158
|
+
)
|
218
159
|
|
219
160
|
@display(description="Usage")
|
220
161
|
def usage_display(self, obj):
|
221
|
-
"""
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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"
|
241
|
-
else:
|
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
|
250
|
-
|
251
|
-
return format_html(
|
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
|
162
|
+
"""Usage count display."""
|
163
|
+
# This would need actual usage tracking implementation
|
164
|
+
usage_count = getattr(obj, 'usage_count', 0)
|
165
|
+
|
166
|
+
return self.display_count_simple(
|
167
|
+
type('obj', (), {'usage_count': usage_count})(),
|
168
|
+
'usage_count',
|
169
|
+
'requests',
|
170
|
+
CounterBadgeConfig(use_humanize=True)
|
262
171
|
)
|
263
172
|
|
264
|
-
@display(description="
|
265
|
-
def
|
266
|
-
"""
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
173
|
+
@display(description="Created")
|
174
|
+
def created_display(self, obj):
|
175
|
+
"""Created time display."""
|
176
|
+
return self.display_datetime_relative(
|
177
|
+
obj,
|
178
|
+
'created_at',
|
179
|
+
DateTimeDisplayConfig(show_relative=True, show_seconds=False)
|
180
|
+
)
|
181
|
+
|
182
|
+
# Readonly field displays
|
183
|
+
def key_details_display(self, obj):
|
184
|
+
"""Detailed API key information for detail view."""
|
185
|
+
if not obj.pk:
|
186
|
+
return "Save to see details"
|
276
187
|
|
277
|
-
|
278
|
-
|
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
|
-
)
|
188
|
+
from django.utils.html import format_html
|
189
|
+
from django_cfg.modules.django_admin.utils.displays import DateTimeDisplay
|
286
190
|
|
287
|
-
|
191
|
+
details = []
|
288
192
|
|
289
|
-
|
290
|
-
|
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 = "â
"
|
193
|
+
# Full key (be careful in production!)
|
194
|
+
details.append(f"<strong>Full Key:</strong> <code>{obj.key}</code>")
|
298
195
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
color,
|
305
|
-
icon,
|
306
|
-
naturaltime(obj.expires_at),
|
307
|
-
obj.expires_at.strftime('%Y-%m-%d')
|
308
|
-
)
|
309
|
-
|
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>'
|
196
|
+
# Usage statistics
|
197
|
+
if obj.last_used_at:
|
198
|
+
last_used_html = DateTimeDisplay.relative(
|
199
|
+
obj.last_used_at,
|
200
|
+
DateTimeDisplayConfig(show_relative=True)
|
319
201
|
)
|
202
|
+
details.append(f"<strong>Last Used:</strong> {last_used_html}")
|
203
|
+
else:
|
204
|
+
details.append("<strong>Last Used:</strong> Never")
|
320
205
|
|
321
|
-
|
322
|
-
|
206
|
+
# Age calculation
|
207
|
+
age = timezone.now() - obj.created_at
|
208
|
+
age_text = f"{age.days} days old"
|
209
|
+
details.append(f"<strong>Age:</strong> {age_text}")
|
323
210
|
|
324
|
-
|
325
|
-
|
326
|
-
|
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"
|
211
|
+
# Security info
|
212
|
+
if obj.is_active:
|
213
|
+
security_status = '<span class="text-green-600">đ Active</span>'
|
340
214
|
else:
|
341
|
-
|
342
|
-
icon = "đ´"
|
343
|
-
status = "Inactive"
|
215
|
+
security_status = '<span class="text-red-600">đ Inactive</span>'
|
344
216
|
|
345
|
-
|
346
|
-
'<div class="text-center {}">'
|
347
|
-
'<div><span class="mr-1">{}</span>{}</div>'
|
348
|
-
'<div class="text-xs">{}</div>'
|
349
|
-
'</div>',
|
350
|
-
color,
|
351
|
-
icon,
|
352
|
-
naturaltime(obj.last_used_at),
|
353
|
-
status
|
354
|
-
)
|
355
|
-
|
356
|
-
@display(description="Created", ordering='created_at')
|
357
|
-
def created_at_display(self, obj):
|
358
|
-
"""Display creation date."""
|
359
|
-
return format_html(
|
360
|
-
'<div class="text-xs">'
|
361
|
-
'<div>{}</div>'
|
362
|
-
'<div class="text-gray-500">{}</div>'
|
363
|
-
'</div>',
|
364
|
-
obj.created_at.strftime('%Y-%m-%d'),
|
365
|
-
naturaltime(obj.created_at)
|
366
|
-
)
|
367
|
-
|
368
|
-
def changelist_view(self, request, extra_context=None):
|
369
|
-
"""Add API key statistics to changelist context."""
|
370
|
-
extra_context = extra_context or {}
|
217
|
+
details.append(f"<strong>Security Status:</strong> {security_status}")
|
371
218
|
|
372
|
-
|
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)
|
219
|
+
return format_html("<br>".join(details))
|
433
220
|
|
434
|
-
|
221
|
+
key_details_display.short_description = "Key Details"
|
435
222
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
223
|
+
# Actions
|
224
|
+
@action(description="Activate selected keys", variant=ActionVariant.SUCCESS)
|
225
|
+
def activate_keys(self, request, queryset):
|
226
|
+
"""Activate selected API keys."""
|
227
|
+
updated = queryset.update(is_active=True)
|
228
|
+
self.message_user(
|
229
|
+
request,
|
230
|
+
f"Successfully activated {updated} API key(s).",
|
231
|
+
level='SUCCESS'
|
232
|
+
)
|
233
|
+
|
234
|
+
@action(description="Deactivate selected keys", variant=ActionVariant.WARNING)
|
441
235
|
def deactivate_keys(self, request, queryset):
|
442
236
|
"""Deactivate selected API keys."""
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
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
|
-
)
|
471
|
-
|
472
|
-
@action(
|
473
|
-
description="đ
Extend Expiry (30 days)",
|
474
|
-
icon="schedule",
|
475
|
-
variant=ActionVariant.INFO
|
476
|
-
)
|
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(
|
492
|
-
request,
|
493
|
-
f"đ
Extended expiry for {extended_count} API keys by 30 days"
|
494
|
-
)
|
495
|
-
|
496
|
-
@action(
|
497
|
-
description="đ Rotate Keys",
|
498
|
-
icon="refresh",
|
499
|
-
variant=ActionVariant.WARNING
|
500
|
-
)
|
501
|
-
def rotate_keys(self, request, queryset):
|
502
|
-
"""Rotate selected API keys (generate new keys)."""
|
503
|
-
|
504
|
-
rotated_count = 0
|
505
|
-
|
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
|
-
)
|
538
|
-
|
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
|
237
|
+
updated = queryset.update(is_active=False)
|
238
|
+
self.message_user(
|
239
|
+
request,
|
240
|
+
f"Successfully deactivated {updated} API key(s).",
|
241
|
+
level='WARNING'
|
552
242
|
)
|
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
|
-
)
|
582
243
|
|
583
|
-
@action(
|
584
|
-
|
585
|
-
|
586
|
-
|
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
|
-
])
|
244
|
+
@action(description="Regenerate selected keys", variant=ActionVariant.DANGER)
|
245
|
+
def regenerate_keys(self, request, queryset):
|
246
|
+
"""Regenerate selected API keys."""
|
247
|
+
import secrets
|
248
|
+
import string
|
602
249
|
|
250
|
+
updated_count = 0
|
603
251
|
for api_key in queryset:
|
604
|
-
#
|
605
|
-
|
606
|
-
|
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'
|
252
|
+
# Generate new key
|
253
|
+
alphabet = string.ascii_letters + string.digits
|
254
|
+
new_key = ''.join(secrets.choice(alphabet) for _ in range(32))
|
613
255
|
|
614
|
-
|
615
|
-
|
616
|
-
|
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
|
-
])
|
256
|
+
api_key.key = new_key
|
257
|
+
api_key.save()
|
258
|
+
updated_count += 1
|
625
259
|
|
626
|
-
|
260
|
+
self.message_user(
|
627
261
|
request,
|
628
|
-
f"
|
262
|
+
f"Successfully regenerated {updated_count} API key(s).",
|
263
|
+
level='WARNING'
|
629
264
|
)
|
630
|
-
|
631
|
-
return response
|