django-cfg 1.4.82__py3-none-any.whl → 1.4.84__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/centrifugo/serializers/channels.py +3 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +2 -0
- django_cfg/apps/centrifugo/views/admin_api.py +1 -1
- django_cfg/apps/centrifugo/views/monitoring.py +116 -6
- django_cfg/apps/centrifugo/views/testing_api.py +1 -1
- django_cfg/apps/dashboard/__init__.py +8 -0
- django_cfg/apps/dashboard/api/__init__.py +27 -0
- django_cfg/apps/dashboard/api/serializers.py +165 -0
- django_cfg/apps/dashboard/api/viewsets.py +257 -0
- django_cfg/apps/dashboard/apps.py +23 -0
- django_cfg/apps/dashboard/services/__init__.py +11 -0
- django_cfg/apps/dashboard/services/statistics_service.py +235 -0
- django_cfg/apps/dashboard/services/system_health_service.py +280 -0
- django_cfg/apps/dashboard/urls.py +23 -0
- django_cfg/apps/frontend/JWT_AUTO_INJECTION.md +224 -0
- django_cfg/apps/frontend/views.py +121 -7
- django_cfg/apps/tasks/api/serializers.py +82 -0
- django_cfg/apps/tasks/api/views.py +571 -0
- django_cfg/apps/urls.py +2 -1
- django_cfg/core/builders/apps_builder.py +1 -0
- django_cfg/middleware/README.md +12 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +1 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +7 -10
- django_cfg/modules/django_client/core/parser/openapi30.py +26 -1
- django_cfg/modules/django_client/core/parser/openapi31.py +26 -1
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500.html +1 -1
- django_cfg/static/frontend/admin/_next/static/-Zk0eDB7OJOEFrFyR5BwZ/_buildManifest.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{43076.55dd23b6cd68edb0.js → 20695.a7d37b6c40ad3f58.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{25033.d626f78bc99bc4a1.js → 25033.ee3e206d5a2877b6.js} +2 -2
- django_cfg/static/frontend/admin/_next/static/chunks/{25892.964150a58f94ce06.js → 25892.5cbed319f9226fdc.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{2d7a934f.dfef67639279d59d.js → 2d7a934f.329c61f23af1a7ec.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{30649.00c679812a56aee3.js → 30649.963cfb7268b5864a.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{30875.784491146c38dbcb.js → 30875.82c3741757b8aa32.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{32163.ab0ca435b3f26c04.js → 32163.109a03a7252f1508.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/43076-4be6a9794e9c3e8b.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{49978.fb8ba7ee52ffe666.js → 49978.db5a86a8eb233f35.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-79c02212788f1ec7.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{50319.f786248384877960.js → 50319.fd78c7f7e3f1966e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{52908.b690e323d8f8efdd.js → 52908.da5b850b0bc0970c.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{53710.80ca863525d137db.js → 53710.7176bbee6c7b78be.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{57982.251fed8d58adcf53.js → 57982.2c90b33b0934522a.js} +2 -2
- django_cfg/static/frontend/admin/_next/static/chunks/{60181.86e18057c4caaa97.js → 60181.c94d78d10eb5da37.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{60374.bde0ec1249aa79c6.js → 60374.5d80cfc45439b2b0.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{6884.7b1db804c88280ed.js → 6884.624d563508cf6db4.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{69436.9515b854cdf4b57a.js → 69436.be44021e3d7c99c7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{70628.00cdd98f672e684f.js → 70628.58e8c38a66543d5e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{73218.a826c2248612b37f.js → 73218.d712e7bd678e23a8.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{76334.64fbaa923d9ac293.js → 76334.f43f2d8b4bbf8dd6.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{7799.2b280f8ddf067d49.js → 7799.1575cc212bc750c7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{80574.620a8a5b4eb91c25.js → 80574.92638dd7b9979664.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{81127.a0603c3394892d4e.js → 81127.3ead500eec887152.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/82296-a2c8d38f62224be5.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{8383.eb6188b22c453e14.js → 8383.e25a442df26b2e26.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{85833.35e6ca25ac32a7d2.js → 85833.b0dead4fbcbfdd1b.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{95365.fc9d7653a78839d0.js → 95365.2b430045fc2e5acf.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{96424.0793b94836eb13a6.js → 96424.11d76570e9a94b85.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/404-c283223d1afd02a2.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/500-389d6d3e1f2f7fda.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-f25bec36bbdc9625.js +272 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_error-5291033275c26d09.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-d7bc30185f52cbca.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-24588bf5551f30df.js → cookies-b39c7f22c066e2c6.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-354dae34a4c4da59.js → privacy-5aedad0cf3a4f80f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-0a5d7fa591ebb1ae.js → security-dbd854d0d5d483e2.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-c3d80322f52dc112.js → terms-f3e1d2b9e5edf12f.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-22532c65971225eb.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/profile-e93a65e8e7d9022b.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/ui-669e8f2a785beba2.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-a8a9ba76f2c75354.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{webpack-905bba30877f6490.js → webpack-92add5f95c66e349.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/css/78d677ac1677c210.css +3 -0
- django_cfg/static/frontend/admin/auth.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy.html +1 -1
- django_cfg/static/frontend/admin/legal/security.html +1 -1
- django_cfg/static/frontend/admin/legal/terms.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo.html +1 -0
- django_cfg/static/frontend/admin/private/profile.html +1 -0
- django_cfg/static/frontend/admin/private/ui.html +1 -0
- django_cfg/static/frontend/admin/private.html +1 -1
- django_cfg/templates/admin/index.html +97 -63
- django_cfg/templates/admin_old/index.html +80 -0
- django_cfg/templatetags/django_cfg.py +57 -10
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/METADATA +1 -1
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/RECORD +142 -122
- django_cfg/static/frontend/admin/_next/static/chunks/pages/404-7cdad2942c3fb179.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/500-6cdb27b00678364f.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-9c5ca2471de6b000.js +0 -272
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_error-b8071a05cabe1c2d.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/index-bf88192a30e013a9.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-2f58633ddf63a5bc.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/ui-73632f2d9c6b11ab.js +0 -1
- django_cfg/static/frontend/admin/_next/static/css/e201974f9a4d64e6.css +0 -3
- django_cfg/static/frontend/admin/_next/static/qEBrQJUidlI_maQ4xQnI0/_buildManifest.js +0 -1
- django_cfg/static/frontend/admin/ui.html +0 -92
- /django_cfg/static/frontend/admin/_next/static/{qEBrQJUidlI_maQ4xQnI0 → -Zk0eDB7OJOEFrFyR5BwZ}/_ssgManifest.js +0 -0
- /django_cfg/templates/{admin → admin_old}/components/action_grid.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/card.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/data_table.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/metric_card.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/modal.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/progress_bar.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/section_header.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/stat_item.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/stats_grid.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/status_badge.html +0 -0
- /django_cfg/templates/{admin → admin_old}/components/user_avatar.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/change_list.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/default_value.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/fieldset_header.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/results_list.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/setting_row.html +0 -0
- /django_cfg/templates/{admin → admin_old}/constance/includes/table_headers.html +0 -0
- /django_cfg/templates/{admin → admin_old}/examples/component_class_example.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_export.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_import.html +0 -0
- /django_cfg/templates/{admin → admin_old}/import_export/change_list_import_export.html +0 -0
- /django_cfg/templates/{admin → admin_old}/index_new.html +0 -0
- /django_cfg/templates/{admin → admin_old}/layouts/base_dashboard.html +0 -0
- /django_cfg/templates/{admin → admin_old}/layouts/dashboard_with_tabs.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/commands_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/documentation_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/overview_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/stats_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/system_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/sections/widgets_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/activity_tracker.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/charts_section.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/django_commands.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/quick_actions.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/recent_activity_improved.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/recent_users_table.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/stats_cards.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/stats_tiles.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/system_health.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/system_metrics.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/components/user_permissions.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/app_stats_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/commands_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/documentation_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/overview_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/stats_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/users_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/tabs/widgets_tab.html +0 -0
- /django_cfg/templates/{admin → admin_old}/snippets/zones/zones_table.html +0 -0
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.82.dist-info → django_cfg-1.4.84.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System Health Service
|
|
3
|
+
|
|
4
|
+
Monitors system components health status.
|
|
5
|
+
Checks database, cache, queue, storage, and API availability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Literal
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SystemHealthService:
|
|
16
|
+
"""
|
|
17
|
+
Service for monitoring system component health.
|
|
18
|
+
|
|
19
|
+
%%PRIORITY:HIGH%%
|
|
20
|
+
%%AI_HINT: Checks health of various system components%%
|
|
21
|
+
|
|
22
|
+
TAGS: health, monitoring, system, service
|
|
23
|
+
DEPENDS_ON: [django.db.connection, django.core.cache, redis]
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize system health service."""
|
|
28
|
+
self.logger = logger
|
|
29
|
+
|
|
30
|
+
def check_database_health(self) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Check database connectivity and health.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Health status dictionary with status, description, last_check
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
from django.db import connection
|
|
39
|
+
|
|
40
|
+
# Test database connection
|
|
41
|
+
with connection.cursor() as cursor:
|
|
42
|
+
cursor.execute("SELECT 1")
|
|
43
|
+
cursor.fetchone()
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
'component': 'database',
|
|
47
|
+
'status': 'healthy',
|
|
48
|
+
'description': 'Database connection is working',
|
|
49
|
+
'last_check': datetime.now().isoformat(),
|
|
50
|
+
'health_percentage': 100,
|
|
51
|
+
}
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.logger.error(f"Database health check failed: {e}")
|
|
54
|
+
return {
|
|
55
|
+
'component': 'database',
|
|
56
|
+
'status': 'error',
|
|
57
|
+
'description': f'Database error: {str(e)}',
|
|
58
|
+
'last_check': datetime.now().isoformat(),
|
|
59
|
+
'health_percentage': 0,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def check_cache_health(self) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Check cache (Redis/Memcached) connectivity and health.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Health status dictionary
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
from django.core.cache import cache
|
|
71
|
+
|
|
72
|
+
# Test cache by setting and getting a test value
|
|
73
|
+
test_key = 'health_check_test'
|
|
74
|
+
test_value = 'ok'
|
|
75
|
+
cache.set(test_key, test_value, timeout=10)
|
|
76
|
+
result = cache.get(test_key)
|
|
77
|
+
|
|
78
|
+
if result == test_value:
|
|
79
|
+
cache.delete(test_key)
|
|
80
|
+
return {
|
|
81
|
+
'component': 'cache',
|
|
82
|
+
'status': 'healthy',
|
|
83
|
+
'description': 'Cache is working correctly',
|
|
84
|
+
'last_check': datetime.now().isoformat(),
|
|
85
|
+
'health_percentage': 100,
|
|
86
|
+
}
|
|
87
|
+
else:
|
|
88
|
+
return {
|
|
89
|
+
'component': 'cache',
|
|
90
|
+
'status': 'warning',
|
|
91
|
+
'description': 'Cache test failed',
|
|
92
|
+
'last_check': datetime.now().isoformat(),
|
|
93
|
+
'health_percentage': 50,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.error(f"Cache health check failed: {e}")
|
|
98
|
+
return {
|
|
99
|
+
'component': 'cache',
|
|
100
|
+
'status': 'error',
|
|
101
|
+
'description': f'Cache error: {str(e)}',
|
|
102
|
+
'last_check': datetime.now().isoformat(),
|
|
103
|
+
'health_percentage': 0,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def check_queue_health(self) -> Dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Check task queue (Celery/Dramatiq) health.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Health status dictionary
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
# TODO: Add real queue health check
|
|
115
|
+
# Example: Check Redis connection, queue sizes, worker status
|
|
116
|
+
from django_cfg.modules.django_tasks import DjangoTasks
|
|
117
|
+
|
|
118
|
+
tasks = DjangoTasks()
|
|
119
|
+
redis_client = tasks.get_redis_client()
|
|
120
|
+
|
|
121
|
+
if redis_client and redis_client.ping():
|
|
122
|
+
return {
|
|
123
|
+
'component': 'queue',
|
|
124
|
+
'status': 'healthy',
|
|
125
|
+
'description': 'Queue system is operational',
|
|
126
|
+
'last_check': datetime.now().isoformat(),
|
|
127
|
+
'health_percentage': 100,
|
|
128
|
+
}
|
|
129
|
+
else:
|
|
130
|
+
return {
|
|
131
|
+
'component': 'queue',
|
|
132
|
+
'status': 'error',
|
|
133
|
+
'description': 'Queue system unavailable',
|
|
134
|
+
'last_check': datetime.now().isoformat(),
|
|
135
|
+
'health_percentage': 0,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self.logger.error(f"Queue health check failed: {e}")
|
|
140
|
+
return {
|
|
141
|
+
'component': 'queue',
|
|
142
|
+
'status': 'error',
|
|
143
|
+
'description': f'Queue error: {str(e)}',
|
|
144
|
+
'last_check': datetime.now().isoformat(),
|
|
145
|
+
'health_percentage': 0,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def check_storage_health(self) -> Dict[str, Any]:
|
|
149
|
+
"""
|
|
150
|
+
Check storage/file system health.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Health status dictionary
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
import os
|
|
157
|
+
from django.conf import settings
|
|
158
|
+
|
|
159
|
+
# Check if media directory is writable
|
|
160
|
+
media_root = getattr(settings, 'MEDIA_ROOT', None)
|
|
161
|
+
|
|
162
|
+
if media_root and os.path.exists(media_root) and os.access(media_root, os.W_OK):
|
|
163
|
+
return {
|
|
164
|
+
'component': 'storage',
|
|
165
|
+
'status': 'healthy',
|
|
166
|
+
'description': 'Storage is accessible and writable',
|
|
167
|
+
'last_check': datetime.now().isoformat(),
|
|
168
|
+
'health_percentage': 100,
|
|
169
|
+
}
|
|
170
|
+
else:
|
|
171
|
+
return {
|
|
172
|
+
'component': 'storage',
|
|
173
|
+
'status': 'warning',
|
|
174
|
+
'description': 'Storage may have limited access',
|
|
175
|
+
'last_check': datetime.now().isoformat(),
|
|
176
|
+
'health_percentage': 70,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self.logger.error(f"Storage health check failed: {e}")
|
|
181
|
+
return {
|
|
182
|
+
'component': 'storage',
|
|
183
|
+
'status': 'error',
|
|
184
|
+
'description': f'Storage error: {str(e)}',
|
|
185
|
+
'last_check': datetime.now().isoformat(),
|
|
186
|
+
'health_percentage': 0,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def get_all_health_checks(self) -> List[Dict[str, Any]]:
|
|
190
|
+
"""
|
|
191
|
+
Run all health checks and return aggregated results.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of health check results for all components
|
|
195
|
+
|
|
196
|
+
USED_BY: DashboardViewSet.system_health endpoint
|
|
197
|
+
"""
|
|
198
|
+
checks = [
|
|
199
|
+
self.check_database_health(),
|
|
200
|
+
self.check_cache_health(),
|
|
201
|
+
self.check_queue_health(),
|
|
202
|
+
self.check_storage_health(),
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
return checks
|
|
206
|
+
|
|
207
|
+
def get_overall_health_status(self) -> Dict[str, Any]:
|
|
208
|
+
"""
|
|
209
|
+
Get overall system health status.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Dictionary with overall status, percentage, and component details
|
|
213
|
+
"""
|
|
214
|
+
checks = self.get_all_health_checks()
|
|
215
|
+
|
|
216
|
+
# Calculate overall health percentage
|
|
217
|
+
total_health = sum(check.get('health_percentage', 0) for check in checks)
|
|
218
|
+
overall_percentage = total_health // len(checks) if checks else 0
|
|
219
|
+
|
|
220
|
+
# Determine overall status
|
|
221
|
+
statuses = [check.get('status') for check in checks]
|
|
222
|
+
if 'error' in statuses:
|
|
223
|
+
overall_status = 'error'
|
|
224
|
+
elif 'warning' in statuses:
|
|
225
|
+
overall_status = 'warning'
|
|
226
|
+
else:
|
|
227
|
+
overall_status = 'healthy'
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
'overall_status': overall_status,
|
|
231
|
+
'overall_health_percentage': overall_percentage,
|
|
232
|
+
'components': checks,
|
|
233
|
+
'timestamp': datetime.now().isoformat(),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
def get_quick_actions(self) -> List[Dict[str, Any]]:
|
|
237
|
+
"""
|
|
238
|
+
Get quick action buttons for dashboard.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of quick action dictionaries
|
|
242
|
+
|
|
243
|
+
%%AI_HINT: Actions link to admin pages or trigger common tasks%%
|
|
244
|
+
"""
|
|
245
|
+
actions = [
|
|
246
|
+
{
|
|
247
|
+
'title': 'User Management',
|
|
248
|
+
'description': 'Manage users and permissions',
|
|
249
|
+
'icon': 'people',
|
|
250
|
+
'link': '/admin/auth/user/',
|
|
251
|
+
'color': 'primary',
|
|
252
|
+
'category': 'admin',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
'title': 'View Logs',
|
|
256
|
+
'description': 'Check system logs',
|
|
257
|
+
'icon': 'description',
|
|
258
|
+
'link': '/admin/django_cfg/logs/',
|
|
259
|
+
'color': 'secondary',
|
|
260
|
+
'category': 'system',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
'title': 'Clear Cache',
|
|
264
|
+
'description': 'Clear application cache',
|
|
265
|
+
'icon': 'refresh',
|
|
266
|
+
'link': '/cfg/admin/cache/clear/',
|
|
267
|
+
'color': 'warning',
|
|
268
|
+
'category': 'system',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
'title': 'Run Backup',
|
|
272
|
+
'description': 'Create system backup',
|
|
273
|
+
'icon': 'backup',
|
|
274
|
+
'link': '/cfg/admin/backup/create/',
|
|
275
|
+
'color': 'success',
|
|
276
|
+
'category': 'system',
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
return actions
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard URLs
|
|
3
|
+
|
|
4
|
+
RESTful API endpoints for dashboard data.
|
|
5
|
+
Follows the same pattern as tasks app.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django.urls import include, path
|
|
9
|
+
from rest_framework.routers import DefaultRouter
|
|
10
|
+
|
|
11
|
+
from .api.viewsets import DashboardViewSet
|
|
12
|
+
|
|
13
|
+
app_name = 'dashboard'
|
|
14
|
+
|
|
15
|
+
# Main router for ViewSets
|
|
16
|
+
router = DefaultRouter()
|
|
17
|
+
router.register(r'', DashboardViewSet, basename='dashboard')
|
|
18
|
+
|
|
19
|
+
urlpatterns = [
|
|
20
|
+
# RESTful API endpoints using ViewSets
|
|
21
|
+
# Mounted at cfg/dashboard/api/
|
|
22
|
+
path('api/', include(router.urls)),
|
|
23
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# JWT Auto-Injection для Next.js приложений
|
|
2
|
+
|
|
3
|
+
## Обзор
|
|
4
|
+
|
|
5
|
+
Django-CFG автоматически инжектирует JWT токены (`auth_token` и `refresh_token`) в `localStorage` для авторизованных пользователей при загрузке Next.js приложений через **NextJSStaticView**.
|
|
6
|
+
|
|
7
|
+
## Как это работает
|
|
8
|
+
|
|
9
|
+
### Автоматическая инжекция в Next.js apps (рекомендуется)
|
|
10
|
+
|
|
11
|
+
**NextJSStaticView** автоматически инжектирует JWT токены во все HTML ответы для авторизованных пользователей.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# urls.py
|
|
15
|
+
from django_cfg.apps.frontend.views import AdminView
|
|
16
|
+
|
|
17
|
+
urlpatterns = [
|
|
18
|
+
path('admin/', include('django_cfg.apps.frontend.urls')), # JWT injection automatic
|
|
19
|
+
]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
При загрузке **любой страницы Next.js приложения**, если пользователь авторизован:
|
|
23
|
+
1. View обслуживает статический файл Next.js
|
|
24
|
+
2. Генерируются JWT токены (access + refresh)
|
|
25
|
+
3. Токены автоматически инжектируются в `<head>` или `<body>` через `<script>` тег
|
|
26
|
+
4. Токены сохраняются в `localStorage`
|
|
27
|
+
|
|
28
|
+
**Преимущества:**
|
|
29
|
+
- Работает только для Next.js приложений (безопасный scope)
|
|
30
|
+
- Не нужно модифицировать templates
|
|
31
|
+
- Централизованная логика в базовом view
|
|
32
|
+
- Не влияет на другие HTML responses (Django admin, etc.)
|
|
33
|
+
|
|
34
|
+
### Template Tags (для кастомных шаблонов)
|
|
35
|
+
|
|
36
|
+
Если вы используете собственные Django шаблоны, можете использовать готовые template tags:
|
|
37
|
+
|
|
38
|
+
#### 1. Полная автоматическая инжекция
|
|
39
|
+
|
|
40
|
+
```django
|
|
41
|
+
{% load django_cfg %}
|
|
42
|
+
|
|
43
|
+
<!DOCTYPE html>
|
|
44
|
+
<html>
|
|
45
|
+
<head>
|
|
46
|
+
<title>My App</title>
|
|
47
|
+
{% inject_jwt_tokens_script %} {# Автоматически инжектит оба токена #}
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<!-- Your content -->
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### 2. Отдельные токены
|
|
56
|
+
|
|
57
|
+
```django
|
|
58
|
+
{% load django_cfg %}
|
|
59
|
+
|
|
60
|
+
<script>
|
|
61
|
+
// Access token
|
|
62
|
+
const accessToken = '{% user_jwt_token %}';
|
|
63
|
+
|
|
64
|
+
// Refresh token
|
|
65
|
+
const refreshToken = '{% user_jwt_refresh_token %}';
|
|
66
|
+
|
|
67
|
+
// Manual storage
|
|
68
|
+
localStorage.setItem('auth_token', accessToken);
|
|
69
|
+
localStorage.setItem('refresh_token', refreshToken);
|
|
70
|
+
</script>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Использование в Next.js
|
|
74
|
+
|
|
75
|
+
После инжекции токены доступны в вашем Next.js приложении:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// В любом Next.js компоненте или API клиенте
|
|
79
|
+
const accessToken = localStorage.getItem('auth_token');
|
|
80
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
81
|
+
|
|
82
|
+
// Использование с API клиентом
|
|
83
|
+
import { API } from './generated/cfg';
|
|
84
|
+
|
|
85
|
+
const api = new API('http://localhost:8000', {
|
|
86
|
+
storage: {
|
|
87
|
+
getItem: (key) => localStorage.getItem(key),
|
|
88
|
+
setItem: (key, value) => localStorage.setItem(key, value),
|
|
89
|
+
removeItem: (key) => localStorage.removeItem(key),
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Безопасность
|
|
95
|
+
|
|
96
|
+
### Что инжектируется
|
|
97
|
+
- `auth_token` - JWT access token (короткий срок жизни)
|
|
98
|
+
- `refresh_token` - JWT refresh token (длинный срок жизни)
|
|
99
|
+
|
|
100
|
+
### Когда инжектируется
|
|
101
|
+
Токены генерируются **только** если:
|
|
102
|
+
1. Пользователь **авторизован** через Django session
|
|
103
|
+
2. Загружается **HTML файл** (не JS, CSS и т.д.)
|
|
104
|
+
3. `rest_framework_simplejwt` установлен
|
|
105
|
+
|
|
106
|
+
### Безопасность токенов
|
|
107
|
+
- Токены генерируются **на лету** при каждом запросе
|
|
108
|
+
- Access token имеет короткий срок жизни (настраивается в `JWTConfig`)
|
|
109
|
+
- Refresh token позволяет получить новый access token без повторной авторизации
|
|
110
|
+
- Токены хранятся только в `localStorage` на клиенте
|
|
111
|
+
|
|
112
|
+
## Конфигурация JWT
|
|
113
|
+
|
|
114
|
+
Настройка времени жизни токенов в `django_cfg`:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from django_cfg.models.api import JWTConfig
|
|
118
|
+
|
|
119
|
+
jwt_config = JWTConfig(
|
|
120
|
+
access_token_lifetime_hours=24, # Access token на 24 часа
|
|
121
|
+
refresh_token_lifetime_days=30, # Refresh token на 30 дней
|
|
122
|
+
rotate_refresh_tokens=True, # Ротация refresh токенов
|
|
123
|
+
blacklist_after_rotation=True, # Блэклист старых токенов
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Отладка
|
|
128
|
+
|
|
129
|
+
Проверьте в консоли браузера:
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
// Проверить наличие токенов
|
|
133
|
+
console.log('Access Token:', localStorage.getItem('auth_token'));
|
|
134
|
+
console.log('Refresh Token:', localStorage.getItem('refresh_token'));
|
|
135
|
+
|
|
136
|
+
// Сообщение об успешной инжекции
|
|
137
|
+
// "JWT tokens injected successfully"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Примеры
|
|
141
|
+
|
|
142
|
+
### Пример 1: Автоматическая инжекция в Next.js приложении (рекомендуется)
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
# urls.py - JWT injection работает автоматически
|
|
146
|
+
urlpatterns = [
|
|
147
|
+
path('cfg/admin/', include('django_cfg.apps.frontend.urls')), # Admin Panel with JWT
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
# views.py - создайте свой Next.js app view
|
|
151
|
+
from django_cfg.apps.frontend.views import NextJSStaticView
|
|
152
|
+
|
|
153
|
+
class MyAppView(NextJSStaticView):
|
|
154
|
+
"""Custom Next.js app with automatic JWT injection."""
|
|
155
|
+
app_name = 'my_app' # Serves from static/frontend/my_app/
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
При переходе на **любую страницу** Next.js приложения авторизованный пользователь автоматически получит JWT токены в localStorage.
|
|
159
|
+
|
|
160
|
+
**⚠️ На /cfg/admin/auth токены НЕ инжектятся** - это страница логина, пользователь ещё не авторизован!
|
|
161
|
+
|
|
162
|
+
### Пример 2: Кастомный шаблон с инжекцией
|
|
163
|
+
|
|
164
|
+
```django
|
|
165
|
+
{% load django_cfg %}
|
|
166
|
+
|
|
167
|
+
<!DOCTYPE html>
|
|
168
|
+
<html>
|
|
169
|
+
<head>
|
|
170
|
+
<meta charset="utf-8">
|
|
171
|
+
<title>Centrifugo Monitor</title>
|
|
172
|
+
|
|
173
|
+
{# Автоматическая инжекция JWT токенов #}
|
|
174
|
+
{% inject_jwt_tokens_script %}
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<div id="root"></div>
|
|
178
|
+
<script src="/_next/static/chunks/main.js"></script>
|
|
179
|
+
</body>
|
|
180
|
+
</html>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Требования
|
|
184
|
+
|
|
185
|
+
- Django с включенной аутентификацией
|
|
186
|
+
- `rest_framework_simplejwt` установлен
|
|
187
|
+
- Пользователь авторизован через Django session
|
|
188
|
+
|
|
189
|
+
## API Reference
|
|
190
|
+
|
|
191
|
+
### Template Tags
|
|
192
|
+
|
|
193
|
+
#### `{% user_jwt_token %}`
|
|
194
|
+
Возвращает JWT access token для текущего пользователя.
|
|
195
|
+
|
|
196
|
+
#### `{% user_jwt_refresh_token %}`
|
|
197
|
+
Возвращает JWT refresh token для текущего пользователя.
|
|
198
|
+
|
|
199
|
+
#### `{% inject_jwt_tokens_script %}`
|
|
200
|
+
Генерирует полный `<script>` тег с автоматической инжекцией обоих токенов в localStorage.
|
|
201
|
+
|
|
202
|
+
### View Classes
|
|
203
|
+
|
|
204
|
+
#### `NextJSStaticView`
|
|
205
|
+
Базовый view для обслуживания Next.js статических сборок с автоматической JWT инжекцией.
|
|
206
|
+
|
|
207
|
+
**Features:**
|
|
208
|
+
- Serves Next.js static export files
|
|
209
|
+
- Automatically injects JWT tokens for authenticated users
|
|
210
|
+
- Tokens injected into HTML responses only
|
|
211
|
+
- Handles Next.js client-side routing (.html fallback)
|
|
212
|
+
|
|
213
|
+
**Usage:**
|
|
214
|
+
```python
|
|
215
|
+
from django_cfg.apps.frontend.views import NextJSStaticView
|
|
216
|
+
|
|
217
|
+
class MyAppView(NextJSStaticView):
|
|
218
|
+
app_name = 'my_app' # Serves from static/frontend/my_app/
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `AdminView`
|
|
222
|
+
Специализированный view для Admin Panel (наследует `NextJSStaticView`).
|
|
223
|
+
|
|
224
|
+
**Built-in JWT injection** - no additional configuration needed.
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
"""Views for serving Next.js static builds.
|
|
1
|
+
"""Views for serving Next.js static builds with automatic JWT injection.
|
|
2
2
|
|
|
3
|
+
JWT tokens are automatically injected into HTML responses for authenticated users.
|
|
4
|
+
This is specific to Next.js frontend apps only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
3
8
|
from pathlib import Path
|
|
4
|
-
from django.http import Http404
|
|
9
|
+
from django.http import Http404, HttpResponse, FileResponse
|
|
5
10
|
from django.views.static import serve
|
|
6
11
|
from django.views import View
|
|
12
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
7
15
|
|
|
8
16
|
|
|
9
17
|
class NextJSStaticView(View):
|
|
10
18
|
"""
|
|
11
|
-
Serve Next.js static build files
|
|
19
|
+
Serve Next.js static build files with automatic JWT token injection.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Serves Next.js static export files
|
|
23
|
+
- Automatically injects JWT tokens for authenticated users
|
|
24
|
+
- Tokens injected into HTML responses only
|
|
25
|
+
- Handles Next.js client-side routing (.html fallback)
|
|
12
26
|
"""
|
|
13
27
|
|
|
14
28
|
app_name = 'admin'
|
|
15
29
|
|
|
16
30
|
def get(self, request, path=''):
|
|
17
|
-
"""Serve static files from Next.js build."""
|
|
31
|
+
"""Serve static files from Next.js build with JWT injection."""
|
|
18
32
|
import django_cfg
|
|
19
33
|
|
|
20
34
|
base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / self.app_name
|
|
@@ -26,17 +40,117 @@ class NextJSStaticView(View):
|
|
|
26
40
|
if not path or path == '/':
|
|
27
41
|
path = 'index.html'
|
|
28
42
|
|
|
43
|
+
# Handle trailing slash (Next.js static export behavior)
|
|
44
|
+
# /private/ -> private.html
|
|
45
|
+
if path.endswith('/') and path != '/':
|
|
46
|
+
path = path.rstrip('/') + '.html'
|
|
47
|
+
|
|
29
48
|
# For routes without extension, try .html (Next.js static export behavior)
|
|
30
49
|
file_path = base_dir / path
|
|
31
50
|
if not file_path.exists() and not path.endswith('.html') and '.' not in Path(path).name:
|
|
32
|
-
# Try adding .html extension
|
|
33
51
|
html_path = path + '.html'
|
|
34
52
|
html_file = base_dir / html_path
|
|
35
53
|
if html_file.exists():
|
|
36
54
|
path = html_path
|
|
37
55
|
|
|
38
|
-
#
|
|
39
|
-
|
|
56
|
+
# For HTML files, remove conditional GET headers to force full response
|
|
57
|
+
# This allows JWT token injection (can't inject into 304 Not Modified responses)
|
|
58
|
+
is_html_file = path.endswith('.html')
|
|
59
|
+
if is_html_file and request.user.is_authenticated:
|
|
60
|
+
request.META.pop('HTTP_IF_MODIFIED_SINCE', None)
|
|
61
|
+
request.META.pop('HTTP_IF_NONE_MATCH', None)
|
|
62
|
+
|
|
63
|
+
# Serve the static file
|
|
64
|
+
response = serve(request, path, document_root=str(base_dir))
|
|
65
|
+
|
|
66
|
+
# Convert FileResponse to HttpResponse for HTML files to enable JWT injection
|
|
67
|
+
if isinstance(response, FileResponse):
|
|
68
|
+
content_type = response.get('Content-Type', '')
|
|
69
|
+
if 'text/html' in content_type and request.user.is_authenticated:
|
|
70
|
+
content = b''.join(response.streaming_content)
|
|
71
|
+
original_response = response
|
|
72
|
+
response = HttpResponse(
|
|
73
|
+
content=content,
|
|
74
|
+
status=original_response.status_code,
|
|
75
|
+
content_type=content_type
|
|
76
|
+
)
|
|
77
|
+
# Copy headers from original response
|
|
78
|
+
for header, value in original_response.items():
|
|
79
|
+
if header.lower() not in ('content-length', 'content-type'):
|
|
80
|
+
response[header] = value
|
|
81
|
+
|
|
82
|
+
# Inject JWT tokens for authenticated users on HTML responses
|
|
83
|
+
if self._should_inject_jwt(request, response):
|
|
84
|
+
self._inject_jwt_tokens(request, response)
|
|
85
|
+
|
|
86
|
+
return response
|
|
87
|
+
|
|
88
|
+
def _should_inject_jwt(self, request, response):
|
|
89
|
+
"""Check if JWT tokens should be injected."""
|
|
90
|
+
# Only for authenticated users
|
|
91
|
+
if not request.user or not request.user.is_authenticated:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# Only for HttpResponse (not FileResponse or StreamingHttpResponse)
|
|
95
|
+
if not isinstance(response, HttpResponse) or isinstance(response, FileResponse):
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# Check if response has content attribute
|
|
99
|
+
if not hasattr(response, 'content'):
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Only for HTML responses
|
|
103
|
+
content_type = response.get('Content-Type', '')
|
|
104
|
+
return 'text/html' in content_type
|
|
105
|
+
|
|
106
|
+
def _inject_jwt_tokens(self, request, response):
|
|
107
|
+
"""Inject JWT tokens into HTML response."""
|
|
108
|
+
try:
|
|
109
|
+
# Generate JWT tokens
|
|
110
|
+
refresh = RefreshToken.for_user(request.user)
|
|
111
|
+
access_token = str(refresh.access_token)
|
|
112
|
+
refresh_token = str(refresh)
|
|
113
|
+
|
|
114
|
+
# Create injection script
|
|
115
|
+
injection_script = f"""
|
|
116
|
+
<script>
|
|
117
|
+
(function() {{
|
|
118
|
+
try {{
|
|
119
|
+
localStorage.setItem('auth_token', '{access_token}');
|
|
120
|
+
localStorage.setItem('refresh_token', '{refresh_token}');
|
|
121
|
+
console.log('[Django-CFG] JWT tokens injected successfully');
|
|
122
|
+
}} catch (e) {{
|
|
123
|
+
console.error('[Django-CFG] Failed to inject JWT tokens:', e);
|
|
124
|
+
}}
|
|
125
|
+
}})();
|
|
126
|
+
</script>
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
# Decode response content
|
|
130
|
+
try:
|
|
131
|
+
content = response.content.decode('utf-8')
|
|
132
|
+
except UnicodeDecodeError:
|
|
133
|
+
logger.warning("Failed to decode response content as UTF-8, skipping JWT injection")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Inject before </head> or </body>
|
|
137
|
+
if '</head>' in content:
|
|
138
|
+
content = content.replace('</head>', f'{injection_script}</head>', 1)
|
|
139
|
+
logger.debug(f"JWT tokens injected before </head> for user {request.user.pk}")
|
|
140
|
+
elif '</body>' in content:
|
|
141
|
+
content = content.replace('</body>', f'{injection_script}</body>', 1)
|
|
142
|
+
logger.debug(f"JWT tokens injected before </body> for user {request.user.pk}")
|
|
143
|
+
else:
|
|
144
|
+
logger.warning(f"No </head> or </body> tag found in HTML, skipping JWT injection")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Update response
|
|
148
|
+
response.content = content.encode('utf-8')
|
|
149
|
+
response['Content-Length'] = len(response.content)
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
# Log error but don't break the response
|
|
153
|
+
logger.error(f"Failed to inject JWT tokens for user {request.user.pk}: {e}", exc_info=True)
|
|
40
154
|
|
|
41
155
|
|
|
42
156
|
class AdminView(NextJSStaticView):
|