django-cfg 1.4.62__py3-none-any.whl → 1.4.64__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/accounts/services/otp_service.py +3 -14
- django_cfg/apps/centrifugo/__init__.py +57 -0
- django_cfg/apps/centrifugo/admin/__init__.py +13 -0
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
- django_cfg/apps/centrifugo/admin/config.py +82 -0
- django_cfg/apps/centrifugo/apps.py +31 -0
- django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
- django_cfg/apps/centrifugo/codegen/README.md +242 -0
- django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
- django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
- django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
- django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
- django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
- django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
- django_cfg/apps/centrifugo/decorators.py +137 -0
- django_cfg/apps/centrifugo/management/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
- django_cfg/apps/centrifugo/managers/__init__.py +12 -0
- django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
- django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
- django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
- django_cfg/apps/centrifugo/models/__init__.py +11 -0
- django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
- django_cfg/apps/centrifugo/registry.py +106 -0
- django_cfg/apps/centrifugo/router.py +125 -0
- django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
- django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
- django_cfg/apps/centrifugo/serializers/channels.py +26 -0
- django_cfg/apps/centrifugo/serializers/health.py +17 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
- django_cfg/apps/centrifugo/serializers/stats.py +21 -0
- django_cfg/apps/centrifugo/services/__init__.py +12 -0
- django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
- django_cfg/apps/centrifugo/services/client/client.py +582 -0
- django_cfg/apps/centrifugo/services/client/config.py +236 -0
- django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
- django_cfg/apps/centrifugo/services/config_helper.py +63 -0
- django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
- django_cfg/apps/centrifugo/services/logging.py +677 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
- django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
- django_cfg/apps/centrifugo/urls.py +31 -0
- django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
- django_cfg/apps/centrifugo/views/__init__.py +15 -0
- django_cfg/apps/centrifugo/views/admin_api.py +380 -0
- django_cfg/apps/centrifugo/views/dashboard.py +15 -0
- django_cfg/apps/centrifugo/views/monitoring.py +286 -0
- django_cfg/apps/centrifugo/views/testing_api.py +422 -0
- django_cfg/apps/support/utils/support_email_service.py +5 -18
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
- django_cfg/apps/urls.py +5 -5
- django_cfg/core/base/config_model.py +4 -44
- django_cfg/core/builders/apps_builder.py +2 -2
- django_cfg/core/generation/integration_generators/third_party.py +8 -8
- django_cfg/core/utils/__init__.py +5 -0
- django_cfg/core/utils/url_helpers.py +73 -0
- django_cfg/modules/base.py +7 -7
- django_cfg/modules/django_client/core/__init__.py +2 -1
- django_cfg/modules/django_client/core/config/config.py +8 -0
- django_cfg/modules/django_client/core/generator/__init__.py +42 -2
- django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
- django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
- django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
- django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
- django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
- django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
- django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
- django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
- django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
- django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
- django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
- django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
- django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
- django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
- django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
- django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
- django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
- django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
- django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
- django_cfg/modules/django_client/system/schema_parser.py +5 -1
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
- django_cfg/modules/django_unfold/dashboard.py +25 -19
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/core.py +2 -0
- django_cfg/registry/modules.py +2 -2
- django_cfg/static/js/api/centrifugo/client.mjs +164 -0
- django_cfg/static/js/api/centrifugo/index.mjs +13 -0
- django_cfg/static/js/api/index.mjs +5 -5
- django_cfg/static/js/api/types.mjs +89 -26
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/METADATA +1 -1
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/RECORD +142 -70
- django_cfg/apps/ipc/README.md +0 -346
- django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
- django_cfg/apps/ipc/TESTING.md +0 -539
- django_cfg/apps/ipc/__init__.py +0 -60
- django_cfg/apps/ipc/admin.py +0 -232
- django_cfg/apps/ipc/apps.py +0 -98
- django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
- django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +0 -229
- django_cfg/apps/ipc/serializers/__init__.py +0 -29
- django_cfg/apps/ipc/serializers/serializers.py +0 -343
- django_cfg/apps/ipc/services/__init__.py +0 -7
- django_cfg/apps/ipc/services/client/__init__.py +0 -23
- django_cfg/apps/ipc/services/client/client.py +0 -621
- django_cfg/apps/ipc/services/client/config.py +0 -214
- django_cfg/apps/ipc/services/client/exceptions.py +0 -201
- django_cfg/apps/ipc/services/logging.py +0 -239
- django_cfg/apps/ipc/services/monitor.py +0 -466
- django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
- django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
- django_cfg/apps/ipc/urls.py +0 -23
- django_cfg/apps/ipc/views/__init__.py +0 -13
- django_cfg/apps/ipc/views/dashboard.py +0 -15
- django_cfg/apps/ipc/views/monitoring.py +0 -251
- django_cfg/apps/ipc/views/testing.py +0 -285
- django_cfg/static/js/api/ipc/client.mjs +0 -114
- django_cfg/static/js/api/ipc/index.mjs +0 -13
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.64.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Template Tags.
|
|
3
|
+
|
|
4
|
+
Provides template tags for accessing Centrifugo configuration in templates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django import template
|
|
8
|
+
from ..services import get_centrifugo_config
|
|
9
|
+
|
|
10
|
+
register = template.Library()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register.simple_tag
|
|
14
|
+
def centrifugo_admin_url():
|
|
15
|
+
"""
|
|
16
|
+
Get Centrifugo admin UI URL from configuration.
|
|
17
|
+
|
|
18
|
+
Returns the HTTP admin URL from centrifugo_api_url.
|
|
19
|
+
Example: http://localhost:8002
|
|
20
|
+
|
|
21
|
+
Usage in template:
|
|
22
|
+
{% load centrifugo_tags %}
|
|
23
|
+
{% centrifugo_admin_url %}
|
|
24
|
+
"""
|
|
25
|
+
config = get_centrifugo_config()
|
|
26
|
+
if not config or not config.centrifugo_api_url:
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
# Return HTTP API URL (admin UI is on same host)
|
|
30
|
+
url = config.centrifugo_api_url
|
|
31
|
+
|
|
32
|
+
# Remove /api suffix if present
|
|
33
|
+
if url.endswith('/api'):
|
|
34
|
+
url = url[:-len('/api')]
|
|
35
|
+
|
|
36
|
+
# Remove trailing slash
|
|
37
|
+
url = url.rstrip('/')
|
|
38
|
+
|
|
39
|
+
return url
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@register.simple_tag
|
|
43
|
+
def centrifugo_is_configured():
|
|
44
|
+
"""
|
|
45
|
+
Check if Centrifugo is configured.
|
|
46
|
+
|
|
47
|
+
Returns True if Centrifugo config exists and has URL.
|
|
48
|
+
|
|
49
|
+
Usage in template:
|
|
50
|
+
{% load centrifugo_tags %}
|
|
51
|
+
{% centrifugo_is_configured as is_configured %}
|
|
52
|
+
{% if is_configured %}...{% endif %}
|
|
53
|
+
"""
|
|
54
|
+
config = get_centrifugo_config()
|
|
55
|
+
return bool(config and config.centrifugo_url)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register.simple_tag
|
|
59
|
+
def centrifugo_wrapper_url():
|
|
60
|
+
"""
|
|
61
|
+
Get Centrifugo wrapper URL from configuration.
|
|
62
|
+
|
|
63
|
+
Returns the wrapper URL (our Django proxy).
|
|
64
|
+
Example: http://localhost:8080
|
|
65
|
+
|
|
66
|
+
Usage in template:
|
|
67
|
+
{% load centrifugo_tags %}
|
|
68
|
+
{% centrifugo_wrapper_url %}
|
|
69
|
+
"""
|
|
70
|
+
config = get_centrifugo_config()
|
|
71
|
+
if not config or not config.wrapper_url:
|
|
72
|
+
return ""
|
|
73
|
+
|
|
74
|
+
return config.wrapper_url.rstrip('/')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"centrifugo_admin_url",
|
|
79
|
+
"centrifugo_is_configured",
|
|
80
|
+
"centrifugo_wrapper_url",
|
|
81
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
URL patterns for Centrifugo module.
|
|
3
|
+
|
|
4
|
+
Public API endpoints for Centrifugo monitoring and admin API proxy.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.urls import include, path
|
|
8
|
+
from rest_framework import routers
|
|
9
|
+
|
|
10
|
+
from .views.admin_api import CentrifugoAdminAPIViewSet
|
|
11
|
+
from .views.monitoring import CentrifugoMonitorViewSet
|
|
12
|
+
from .views.testing_api import CentrifugoTestingAPIViewSet
|
|
13
|
+
|
|
14
|
+
app_name = 'django_cfg_centrifugo'
|
|
15
|
+
|
|
16
|
+
# Create router
|
|
17
|
+
router = routers.DefaultRouter()
|
|
18
|
+
|
|
19
|
+
# Monitoring endpoints (Django logs based)
|
|
20
|
+
router.register(r'monitor', CentrifugoMonitorViewSet, basename='monitor')
|
|
21
|
+
|
|
22
|
+
# Admin API proxy endpoints (Centrifugo server based)
|
|
23
|
+
router.register(r'server', CentrifugoAdminAPIViewSet, basename='server')
|
|
24
|
+
|
|
25
|
+
# Testing API endpoints (live testing from dashboard)
|
|
26
|
+
router.register(r'testing', CentrifugoTestingAPIViewSet, basename='testing')
|
|
27
|
+
|
|
28
|
+
urlpatterns = [
|
|
29
|
+
# Include router URLs
|
|
30
|
+
path('', include(router.urls)),
|
|
31
|
+
]
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Admin URLs for
|
|
2
|
+
Admin URLs for Centrifugo dashboard.
|
|
3
3
|
|
|
4
|
-
Dashboard interface for monitoring
|
|
4
|
+
Dashboard interface for monitoring Centrifugo publish activity.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from django.urls import include, path
|
|
8
8
|
|
|
9
9
|
from .views import dashboard_view
|
|
10
10
|
|
|
11
|
-
app_name = '
|
|
11
|
+
app_name = 'django_cfg_centrifugo_admin'
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
urlpatterns = [
|
|
@@ -16,5 +16,5 @@ urlpatterns = [
|
|
|
16
16
|
path('', dashboard_view, name='dashboard'),
|
|
17
17
|
|
|
18
18
|
# Include API endpoints for dashboard AJAX calls
|
|
19
|
-
path('api/', include('django_cfg.apps.
|
|
19
|
+
path('api/', include('django_cfg.apps.centrifugo.urls')),
|
|
20
20
|
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Views for Centrifugo module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .admin_api import CentrifugoAdminAPIViewSet
|
|
6
|
+
from .dashboard import dashboard_view
|
|
7
|
+
from .monitoring import CentrifugoMonitorViewSet
|
|
8
|
+
from .testing_api import CentrifugoTestingAPIViewSet
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'CentrifugoMonitorViewSet',
|
|
12
|
+
'CentrifugoAdminAPIViewSet',
|
|
13
|
+
'CentrifugoTestingAPIViewSet',
|
|
14
|
+
'dashboard_view',
|
|
15
|
+
]
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Admin API Proxy.
|
|
3
|
+
|
|
4
|
+
Proxies requests to Centrifugo server API with authentication and type safety.
|
|
5
|
+
Provides Django endpoints that map to Centrifugo HTTP API methods.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import httpx
|
|
10
|
+
from django.http import JsonResponse
|
|
11
|
+
from django_cfg.modules.django_logging import get_logger
|
|
12
|
+
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
13
|
+
from rest_framework import status, viewsets
|
|
14
|
+
from rest_framework.authentication import SessionAuthentication
|
|
15
|
+
from rest_framework.decorators import action
|
|
16
|
+
from rest_framework.permissions import IsAdminUser
|
|
17
|
+
from rest_framework.response import Response
|
|
18
|
+
|
|
19
|
+
from ..serializers import (
|
|
20
|
+
CentrifugoInfoRequest,
|
|
21
|
+
CentrifugoInfoResponse,
|
|
22
|
+
CentrifugoChannelsRequest,
|
|
23
|
+
CentrifugoChannelsResponse,
|
|
24
|
+
CentrifugoPresenceRequest,
|
|
25
|
+
CentrifugoPresenceResponse,
|
|
26
|
+
CentrifugoPresenceStatsRequest,
|
|
27
|
+
CentrifugoPresenceStatsResponse,
|
|
28
|
+
CentrifugoHistoryRequest,
|
|
29
|
+
CentrifugoHistoryResponse,
|
|
30
|
+
)
|
|
31
|
+
from ..services import get_centrifugo_config
|
|
32
|
+
|
|
33
|
+
logger = get_logger("centrifugo.admin_api")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CentrifugoAdminAPIViewSet(viewsets.ViewSet):
|
|
37
|
+
"""
|
|
38
|
+
Centrifugo Admin API Proxy ViewSet.
|
|
39
|
+
|
|
40
|
+
Provides type-safe proxy endpoints to Centrifugo server API.
|
|
41
|
+
All requests are authenticated via Django session and proxied to Centrifugo.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
authentication_classes = [SessionAuthentication]
|
|
45
|
+
permission_classes = [IsAdminUser]
|
|
46
|
+
|
|
47
|
+
def __init__(self, *args, **kwargs):
|
|
48
|
+
super().__init__(*args, **kwargs)
|
|
49
|
+
self._http_client = None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def http_client(self) -> httpx.AsyncClient:
|
|
53
|
+
"""Get or create HTTP client for Centrifugo API calls."""
|
|
54
|
+
if self._http_client is None:
|
|
55
|
+
config = get_centrifugo_config()
|
|
56
|
+
if not config:
|
|
57
|
+
raise ValueError("Centrifugo not configured")
|
|
58
|
+
|
|
59
|
+
headers = {"Content-Type": "application/json"}
|
|
60
|
+
if config.centrifugo_api_key:
|
|
61
|
+
# Centrifugo expects "Authorization: apikey <key>" header
|
|
62
|
+
headers["Authorization"] = f"apikey {config.centrifugo_api_key}"
|
|
63
|
+
logger.debug(f"Using Centrifugo API key: {config.centrifugo_api_key[:10]}...")
|
|
64
|
+
else:
|
|
65
|
+
logger.warning("No Centrifugo API key configured!")
|
|
66
|
+
|
|
67
|
+
# Use base URL without /api suffix since we'll add it in requests
|
|
68
|
+
base_url = config.centrifugo_api_url.rstrip('/api').rstrip('/')
|
|
69
|
+
logger.debug(f"Centrifugo API base URL: {base_url}")
|
|
70
|
+
|
|
71
|
+
self._http_client = httpx.AsyncClient(
|
|
72
|
+
base_url=base_url,
|
|
73
|
+
headers=headers,
|
|
74
|
+
timeout=httpx.Timeout(10.0),
|
|
75
|
+
verify=False, # Disable SSL verification for self-signed certs
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return self._http_client
|
|
79
|
+
|
|
80
|
+
async def _call_centrifugo_api(self, method: str, params: dict = None) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Call Centrifugo API method.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
method: API method name (e.g., "info", "channels", "presence")
|
|
86
|
+
params: Method parameters (optional)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
API response data or error dict
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
httpx.HTTPError: If API call fails
|
|
93
|
+
"""
|
|
94
|
+
payload = {"method": method, "params": params or {}}
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
response = await self.http_client.post("/api", json=payload)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
result = response.json()
|
|
100
|
+
|
|
101
|
+
# Return both error and result - let caller decide
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
except httpx.HTTPError as e:
|
|
105
|
+
logger.error(f"Centrifugo API HTTP error: {e}", exc_info=True)
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
@extend_schema(
|
|
109
|
+
tags=["Centrifugo Admin API"],
|
|
110
|
+
summary="Get Centrifugo server info",
|
|
111
|
+
description="Returns server information including node count, version, and uptime.",
|
|
112
|
+
request=CentrifugoInfoRequest,
|
|
113
|
+
responses={
|
|
114
|
+
200: CentrifugoInfoResponse,
|
|
115
|
+
500: {"description": "Server error"},
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
@action(detail=False, methods=["post"], url_path="info")
|
|
119
|
+
def info(self, request):
|
|
120
|
+
"""Get Centrifugo server information."""
|
|
121
|
+
try:
|
|
122
|
+
result = asyncio.run(self._call_centrifugo_api("info", params={}))
|
|
123
|
+
|
|
124
|
+
# Check for Centrifugo API error
|
|
125
|
+
if "error" in result and result["error"]:
|
|
126
|
+
return Response(
|
|
127
|
+
{"error": result["error"], "result": None},
|
|
128
|
+
status=status.HTTP_200_OK # Not a server error, just API error
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Success response
|
|
132
|
+
response = CentrifugoInfoResponse(error=None, result=result.get("result", {}))
|
|
133
|
+
return Response(response.model_dump())
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"Failed to get server info: {e}", exc_info=True)
|
|
137
|
+
return Response(
|
|
138
|
+
{"error": {"code": 102, "message": str(e)}},
|
|
139
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@extend_schema(
|
|
143
|
+
tags=["Centrifugo Admin API"],
|
|
144
|
+
summary="List active channels",
|
|
145
|
+
description="Returns list of active channels with optional pattern filter.",
|
|
146
|
+
request=CentrifugoChannelsRequest,
|
|
147
|
+
responses={
|
|
148
|
+
200: CentrifugoChannelsResponse,
|
|
149
|
+
500: {"description": "Server error"},
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
@action(detail=False, methods=["post"], url_path="channels")
|
|
153
|
+
def channels(self, request):
|
|
154
|
+
"""List active channels."""
|
|
155
|
+
try:
|
|
156
|
+
req_data = CentrifugoChannelsRequest(**request.data)
|
|
157
|
+
result = asyncio.run(self._call_centrifugo_api(
|
|
158
|
+
"channels", params=req_data.model_dump(exclude_none=True)
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
# Check for Centrifugo API error
|
|
162
|
+
if "error" in result and result["error"]:
|
|
163
|
+
return Response(
|
|
164
|
+
{"error": result["error"], "result": None},
|
|
165
|
+
status=status.HTTP_200_OK
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
response = CentrifugoChannelsResponse(error=None, result=result.get("result", {}))
|
|
169
|
+
return Response(response.model_dump())
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Failed to list channels: {e}", exc_info=True)
|
|
173
|
+
return Response(
|
|
174
|
+
{"error": {"code": 102, "message": str(e)}},
|
|
175
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@extend_schema(
|
|
179
|
+
tags=["Centrifugo Admin API"],
|
|
180
|
+
summary="Get channel presence",
|
|
181
|
+
description="Returns list of clients currently subscribed to a channel.",
|
|
182
|
+
request=CentrifugoPresenceRequest,
|
|
183
|
+
responses={
|
|
184
|
+
200: CentrifugoPresenceResponse,
|
|
185
|
+
500: {"description": "Server error"},
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
@action(detail=False, methods=["post"], url_path="presence")
|
|
189
|
+
def presence(self, request):
|
|
190
|
+
"""Get channel presence (active subscribers)."""
|
|
191
|
+
try:
|
|
192
|
+
req_data = CentrifugoPresenceRequest(**request.data)
|
|
193
|
+
result = asyncio.run(self._call_centrifugo_api(
|
|
194
|
+
"presence", params=req_data.model_dump()
|
|
195
|
+
))
|
|
196
|
+
|
|
197
|
+
# Check for Centrifugo API error (e.g., code 108 "not available")
|
|
198
|
+
if "error" in result and result["error"]:
|
|
199
|
+
return Response(
|
|
200
|
+
{"error": result["error"], "result": None},
|
|
201
|
+
status=status.HTTP_200_OK
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
response = CentrifugoPresenceResponse(error=None, result=result.get("result", {}))
|
|
205
|
+
return Response(response.model_dump())
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Failed to get presence: {e}", exc_info=True)
|
|
209
|
+
return Response(
|
|
210
|
+
{"error": {"code": 102, "message": str(e)}},
|
|
211
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
@extend_schema(
|
|
215
|
+
tags=["Centrifugo Admin API"],
|
|
216
|
+
summary="Get channel presence statistics",
|
|
217
|
+
description="Returns quick statistics about channel presence (num_clients, num_users).",
|
|
218
|
+
request=CentrifugoPresenceStatsRequest,
|
|
219
|
+
responses={
|
|
220
|
+
200: CentrifugoPresenceStatsResponse,
|
|
221
|
+
500: {"description": "Server error"},
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
@action(detail=False, methods=["post"], url_path="presence-stats")
|
|
225
|
+
def presence_stats(self, request):
|
|
226
|
+
"""Get channel presence statistics."""
|
|
227
|
+
try:
|
|
228
|
+
req_data = CentrifugoPresenceStatsRequest(**request.data)
|
|
229
|
+
result = asyncio.run(self._call_centrifugo_api(
|
|
230
|
+
"presence_stats", params=req_data.model_dump()
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
# Check for Centrifugo API error (e.g., code 108 "not available")
|
|
234
|
+
if "error" in result and result["error"]:
|
|
235
|
+
return Response(
|
|
236
|
+
{"error": result["error"], "result": None},
|
|
237
|
+
status=status.HTTP_200_OK
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
response = CentrifugoPresenceStatsResponse(error=None, result=result.get("result", {}))
|
|
241
|
+
return Response(response.model_dump())
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Failed to get presence stats: {e}", exc_info=True)
|
|
245
|
+
return Response(
|
|
246
|
+
{"error": {"code": 102, "message": str(e)}},
|
|
247
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@extend_schema(
|
|
251
|
+
tags=["Centrifugo Admin API"],
|
|
252
|
+
summary="Get channel history",
|
|
253
|
+
description="Returns message history for a channel.",
|
|
254
|
+
request=CentrifugoHistoryRequest,
|
|
255
|
+
responses={
|
|
256
|
+
200: CentrifugoHistoryResponse,
|
|
257
|
+
500: {"description": "Server error"},
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
@action(detail=False, methods=["post"], url_path="history")
|
|
261
|
+
def history(self, request):
|
|
262
|
+
"""Get channel message history."""
|
|
263
|
+
try:
|
|
264
|
+
req_data = CentrifugoHistoryRequest(**request.data)
|
|
265
|
+
result = asyncio.run(self._call_centrifugo_api(
|
|
266
|
+
"history", params=req_data.model_dump(exclude_none=True)
|
|
267
|
+
))
|
|
268
|
+
|
|
269
|
+
# Check for Centrifugo API error (e.g., code 108 "not available")
|
|
270
|
+
if "error" in result and result["error"]:
|
|
271
|
+
return Response(
|
|
272
|
+
{"error": result["error"], "result": None},
|
|
273
|
+
status=status.HTTP_200_OK
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
response = CentrifugoHistoryResponse(error=None, result=result.get("result", {}))
|
|
277
|
+
return Response(response.model_dump())
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error(f"Failed to get history: {e}", exc_info=True)
|
|
281
|
+
return Response(
|
|
282
|
+
{"error": {"code": 102, "message": str(e)}},
|
|
283
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@extend_schema(
|
|
287
|
+
tags=["Centrifugo Admin API"],
|
|
288
|
+
summary="Get connection token for dashboard",
|
|
289
|
+
description="Returns JWT token and config for WebSocket connection to Centrifugo.",
|
|
290
|
+
request=None,
|
|
291
|
+
responses={
|
|
292
|
+
200: {
|
|
293
|
+
"type": "object",
|
|
294
|
+
"properties": {
|
|
295
|
+
"token": {"type": "string"},
|
|
296
|
+
"config": {
|
|
297
|
+
"type": "object",
|
|
298
|
+
"properties": {
|
|
299
|
+
"centrifugo_url": {"type": "string"},
|
|
300
|
+
"expires_at": {"type": "string"},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
400: {"description": "Centrifugo not configured"},
|
|
306
|
+
500: {"description": "Server error"},
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
@action(detail=False, methods=["post"], url_path="auth/token")
|
|
310
|
+
def auth_token(self, request):
|
|
311
|
+
"""
|
|
312
|
+
Generate JWT token for dashboard WebSocket connection.
|
|
313
|
+
|
|
314
|
+
Returns token that authenticates the current user to subscribe
|
|
315
|
+
to the dashboard channel for real-time updates.
|
|
316
|
+
"""
|
|
317
|
+
try:
|
|
318
|
+
import jwt
|
|
319
|
+
import time
|
|
320
|
+
from datetime import datetime, timezone
|
|
321
|
+
|
|
322
|
+
config = get_centrifugo_config()
|
|
323
|
+
if not config:
|
|
324
|
+
return Response(
|
|
325
|
+
{"error": "Centrifugo is not configured"},
|
|
326
|
+
status=status.HTTP_400_BAD_REQUEST
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Generate JWT token for current user
|
|
330
|
+
now = int(time.time())
|
|
331
|
+
exp = now + 3600 # 1 hour
|
|
332
|
+
|
|
333
|
+
payload = {
|
|
334
|
+
"sub": str(request.user.id), # User ID
|
|
335
|
+
"exp": exp, # Expiration
|
|
336
|
+
"iat": now, # Issued at
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# Subscribe to dashboard channel
|
|
340
|
+
payload["channels"] = ["centrifugo#dashboard"]
|
|
341
|
+
|
|
342
|
+
# Use HMAC secret from config or Django SECRET_KEY
|
|
343
|
+
secret = config.centrifugo_token_hmac_secret or ""
|
|
344
|
+
if not secret:
|
|
345
|
+
from django.conf import settings
|
|
346
|
+
secret = settings.SECRET_KEY
|
|
347
|
+
|
|
348
|
+
token = jwt.encode(payload, secret, algorithm="HS256")
|
|
349
|
+
|
|
350
|
+
return Response({
|
|
351
|
+
"token": token,
|
|
352
|
+
"config": {
|
|
353
|
+
"centrifugo_url": config.centrifugo_url,
|
|
354
|
+
"expires_at": datetime.fromtimestamp(exp, tz=timezone.utc).isoformat(),
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Failed to generate auth token: {e}", exc_info=True)
|
|
360
|
+
return Response(
|
|
361
|
+
{"error": str(e)},
|
|
362
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def __del__(self):
|
|
366
|
+
"""Cleanup HTTP client on deletion."""
|
|
367
|
+
if self._http_client:
|
|
368
|
+
import asyncio
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
loop = asyncio.get_event_loop()
|
|
372
|
+
if loop.is_running():
|
|
373
|
+
loop.create_task(self._http_client.aclose())
|
|
374
|
+
else:
|
|
375
|
+
loop.run_until_complete(self._http_client.aclose())
|
|
376
|
+
except Exception:
|
|
377
|
+
pass # Ignore cleanup errors
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
__all__ = ["CentrifugoAdminAPIViewSet"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard view for Centrifugo monitoring.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib.admin.views.decorators import staff_member_required
|
|
6
|
+
from django.shortcuts import render
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@staff_member_required
|
|
10
|
+
def dashboard_view(request):
|
|
11
|
+
"""Render the Centrifugo dashboard template."""
|
|
12
|
+
context = {
|
|
13
|
+
'page_title': 'Centrifugo Monitor Dashboard',
|
|
14
|
+
}
|
|
15
|
+
return render(request, 'django_cfg_centrifugo/pages/dashboard.html', context)
|