django-cfg 1.5.14__py3-none-any.whl → 1.5.29__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/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +67 -54
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +261 -13
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Token Generator Service.
|
|
3
|
+
|
|
4
|
+
Provides utilities for generating Centrifugo JWT tokens with user permissions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import jwt
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import List, Dict, Any, Optional
|
|
11
|
+
|
|
12
|
+
from .config_helper import get_centrifugo_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_user_channels(user) -> List[str]:
|
|
16
|
+
"""
|
|
17
|
+
Get list of Centrifugo channels user is allowed to subscribe to.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
user: Django user instance
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of channel names user can access
|
|
24
|
+
|
|
25
|
+
Channel naming convention:
|
|
26
|
+
- user#{user_id} - Personal channel for RPC responses
|
|
27
|
+
- notifications#user#{user_id} - Personal notifications
|
|
28
|
+
- centrifugo#dashboard - Admin dashboard events
|
|
29
|
+
- admin#notifications - Admin notifications
|
|
30
|
+
- grpc#* - All gRPC bot events (admin only)
|
|
31
|
+
- broadcast - Global broadcast channel
|
|
32
|
+
"""
|
|
33
|
+
channels = []
|
|
34
|
+
|
|
35
|
+
# Personal channel for RPC responses
|
|
36
|
+
channels.append(f"user#{user.id}")
|
|
37
|
+
|
|
38
|
+
# Notifications channel
|
|
39
|
+
channels.append(f"notifications#user#{user.id}")
|
|
40
|
+
|
|
41
|
+
# Admin channels
|
|
42
|
+
if user.is_staff or user.is_superuser:
|
|
43
|
+
channels.append("centrifugo#dashboard")
|
|
44
|
+
channels.append("admin#notifications")
|
|
45
|
+
# Allow admins to see all gRPC bot events
|
|
46
|
+
channels.append("grpc#*")
|
|
47
|
+
|
|
48
|
+
# Broadcast channel for all users
|
|
49
|
+
channels.append("broadcast")
|
|
50
|
+
|
|
51
|
+
return channels
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_centrifugo_token(
|
|
55
|
+
user,
|
|
56
|
+
exp_seconds: int = 3600,
|
|
57
|
+
additional_channels: Optional[List[str]] = None
|
|
58
|
+
) -> Dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Generate Centrifugo JWT token with user's allowed channels.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
user: Django user instance
|
|
64
|
+
exp_seconds: Token expiration time in seconds (default: 1 hour)
|
|
65
|
+
additional_channels: Optional additional channels to include
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary with:
|
|
69
|
+
- token: JWT token string
|
|
70
|
+
- centrifugo_url: Centrifugo WebSocket URL
|
|
71
|
+
- expires_at: Token expiration datetime
|
|
72
|
+
- channels: List of allowed channels
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValueError: If Centrifugo is not configured or disabled
|
|
76
|
+
"""
|
|
77
|
+
config = get_centrifugo_config()
|
|
78
|
+
if not config or not config.enabled:
|
|
79
|
+
raise ValueError("Centrifugo not configured or disabled")
|
|
80
|
+
|
|
81
|
+
# Get user's allowed channels
|
|
82
|
+
channels = get_user_channels(user)
|
|
83
|
+
|
|
84
|
+
# Add additional channels if provided
|
|
85
|
+
if additional_channels:
|
|
86
|
+
channels.extend(additional_channels)
|
|
87
|
+
# Remove duplicates while preserving order
|
|
88
|
+
channels = list(dict.fromkeys(channels))
|
|
89
|
+
|
|
90
|
+
# Generate JWT token
|
|
91
|
+
now = int(time.time())
|
|
92
|
+
exp = now + exp_seconds
|
|
93
|
+
|
|
94
|
+
payload = {
|
|
95
|
+
"sub": str(user.id), # User ID
|
|
96
|
+
"exp": exp, # Expiration time
|
|
97
|
+
"iat": now, # Issued at
|
|
98
|
+
"channels": channels, # Allowed channels
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Sign token with HMAC secret
|
|
102
|
+
token = jwt.encode(
|
|
103
|
+
payload,
|
|
104
|
+
config.centrifugo_token_hmac_secret,
|
|
105
|
+
algorithm="HS256"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Use timezone-aware datetime for proper ISO 8601 format
|
|
109
|
+
expires_at = datetime.fromtimestamp(exp, tz=timezone.utc)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"token": token,
|
|
113
|
+
"centrifugo_url": config.centrifugo_url,
|
|
114
|
+
"expires_at": expires_at,
|
|
115
|
+
"channels": channels,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
"get_user_channels",
|
|
121
|
+
"generate_centrifugo_token",
|
|
122
|
+
]
|
|
@@ -10,6 +10,8 @@ from rest_framework import routers
|
|
|
10
10
|
from .views.admin_api import CentrifugoAdminAPIViewSet
|
|
11
11
|
from .views.monitoring import CentrifugoMonitorViewSet
|
|
12
12
|
from .views.testing_api import CentrifugoTestingAPIViewSet
|
|
13
|
+
from .views.token_api import CentrifugoTokenViewSet
|
|
14
|
+
from .views.wrapper import PublishWrapperView
|
|
13
15
|
|
|
14
16
|
app_name = 'django_cfg_centrifugo'
|
|
15
17
|
|
|
@@ -25,7 +27,13 @@ router.register(r'server', CentrifugoAdminAPIViewSet, basename='server')
|
|
|
25
27
|
# Testing API endpoints (live testing from dashboard)
|
|
26
28
|
router.register(r'testing', CentrifugoTestingAPIViewSet, basename='testing')
|
|
27
29
|
|
|
30
|
+
# Token API endpoints (JWT token generation for client connections)
|
|
31
|
+
router.register(r'auth', CentrifugoTokenViewSet, basename='auth')
|
|
32
|
+
|
|
28
33
|
urlpatterns = [
|
|
34
|
+
# Wrapper API endpoint (for CentrifugoClient)
|
|
35
|
+
path('api/publish', PublishWrapperView.as_view(), name='wrapper_publish'),
|
|
36
|
+
|
|
29
37
|
# Include router URLs
|
|
30
38
|
path('', include(router.urls)),
|
|
31
39
|
]
|
|
@@ -5,9 +5,11 @@ Views for Centrifugo module.
|
|
|
5
5
|
from .admin_api import CentrifugoAdminAPIViewSet
|
|
6
6
|
from .monitoring import CentrifugoMonitorViewSet
|
|
7
7
|
from .testing_api import CentrifugoTestingAPIViewSet
|
|
8
|
+
from .wrapper import PublishWrapperView
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
'CentrifugoMonitorViewSet',
|
|
11
12
|
'CentrifugoAdminAPIViewSet',
|
|
12
13
|
'CentrifugoTestingAPIViewSet',
|
|
14
|
+
'PublishWrapperView',
|
|
13
15
|
]
|
|
@@ -5,7 +5,6 @@ Proxies requests to Centrifugo server API with authentication and type safety.
|
|
|
5
5
|
Provides Django endpoints that map to Centrifugo HTTP API methods.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import asyncio
|
|
9
8
|
import httpx
|
|
10
9
|
from django.http import JsonResponse
|
|
11
10
|
from django_cfg.modules.django_logging import get_logger
|
|
@@ -113,10 +112,10 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
113
112
|
},
|
|
114
113
|
)
|
|
115
114
|
@action(detail=False, methods=["post"], url_path="info")
|
|
116
|
-
def info(self, request):
|
|
117
|
-
"""Get Centrifugo server information."""
|
|
115
|
+
async def info(self, request):
|
|
116
|
+
"""Get Centrifugo server information (ASYNC)."""
|
|
118
117
|
try:
|
|
119
|
-
result =
|
|
118
|
+
result = await self._call_centrifugo_api("info", params={})
|
|
120
119
|
|
|
121
120
|
# Check for Centrifugo API error
|
|
122
121
|
if "error" in result and result["error"]:
|
|
@@ -147,13 +146,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
147
146
|
},
|
|
148
147
|
)
|
|
149
148
|
@action(detail=False, methods=["post"], url_path="channels")
|
|
150
|
-
def channels(self, request):
|
|
151
|
-
"""List active channels."""
|
|
149
|
+
async def channels(self, request):
|
|
150
|
+
"""List active channels (ASYNC)."""
|
|
152
151
|
try:
|
|
153
152
|
req_data = CentrifugoChannelsRequest(**request.data)
|
|
154
|
-
result =
|
|
153
|
+
result = await self._call_centrifugo_api(
|
|
155
154
|
"channels", params=req_data.model_dump(exclude_none=True)
|
|
156
|
-
)
|
|
155
|
+
)
|
|
157
156
|
|
|
158
157
|
# Check for Centrifugo API error
|
|
159
158
|
if "error" in result and result["error"]:
|
|
@@ -183,13 +182,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
183
182
|
},
|
|
184
183
|
)
|
|
185
184
|
@action(detail=False, methods=["post"], url_path="presence")
|
|
186
|
-
def presence(self, request):
|
|
187
|
-
"""Get channel presence (active subscribers)."""
|
|
185
|
+
async def presence(self, request):
|
|
186
|
+
"""Get channel presence (active subscribers) (ASYNC)."""
|
|
188
187
|
try:
|
|
189
188
|
req_data = CentrifugoPresenceRequest(**request.data)
|
|
190
|
-
result =
|
|
189
|
+
result = await self._call_centrifugo_api(
|
|
191
190
|
"presence", params=req_data.model_dump()
|
|
192
|
-
)
|
|
191
|
+
)
|
|
193
192
|
|
|
194
193
|
# Check for Centrifugo API error (e.g., code 108 "not available")
|
|
195
194
|
if "error" in result and result["error"]:
|
|
@@ -219,13 +218,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
219
218
|
},
|
|
220
219
|
)
|
|
221
220
|
@action(detail=False, methods=["post"], url_path="presence-stats")
|
|
222
|
-
def presence_stats(self, request):
|
|
223
|
-
"""Get channel presence statistics."""
|
|
221
|
+
async def presence_stats(self, request):
|
|
222
|
+
"""Get channel presence statistics (ASYNC)."""
|
|
224
223
|
try:
|
|
225
224
|
req_data = CentrifugoPresenceStatsRequest(**request.data)
|
|
226
|
-
result =
|
|
225
|
+
result = await self._call_centrifugo_api(
|
|
227
226
|
"presence_stats", params=req_data.model_dump()
|
|
228
|
-
)
|
|
227
|
+
)
|
|
229
228
|
|
|
230
229
|
# Check for Centrifugo API error (e.g., code 108 "not available")
|
|
231
230
|
if "error" in result and result["error"]:
|
|
@@ -255,13 +254,13 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
255
254
|
},
|
|
256
255
|
)
|
|
257
256
|
@action(detail=False, methods=["post"], url_path="history")
|
|
258
|
-
def history(self, request):
|
|
259
|
-
"""Get channel message history."""
|
|
257
|
+
async def history(self, request):
|
|
258
|
+
"""Get channel message history (ASYNC)."""
|
|
260
259
|
try:
|
|
261
260
|
req_data = CentrifugoHistoryRequest(**request.data)
|
|
262
|
-
result =
|
|
261
|
+
result = await self._call_centrifugo_api(
|
|
263
262
|
"history", params=req_data.model_dump(exclude_none=True)
|
|
264
|
-
)
|
|
263
|
+
)
|
|
265
264
|
|
|
266
265
|
# Check for Centrifugo API error (e.g., code 108 "not available")
|
|
267
266
|
if "error" in result and result["error"]:
|
|
@@ -359,19 +358,17 @@ class CentrifugoAdminAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
359
358
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
360
359
|
)
|
|
361
360
|
|
|
362
|
-
def
|
|
363
|
-
"""
|
|
361
|
+
async def cleanup(self):
|
|
362
|
+
"""
|
|
363
|
+
Explicit async cleanup method for HTTP client.
|
|
364
|
+
|
|
365
|
+
Note: Django handles ViewSet lifecycle automatically.
|
|
366
|
+
This method is provided for explicit cleanup if needed,
|
|
367
|
+
but httpx.AsyncClient will be garbage collected normally.
|
|
368
|
+
"""
|
|
364
369
|
if self._http_client:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
try:
|
|
368
|
-
loop = asyncio.get_event_loop()
|
|
369
|
-
if loop.is_running():
|
|
370
|
-
loop.create_task(self._http_client.aclose())
|
|
371
|
-
else:
|
|
372
|
-
loop.run_until_complete(self._http_client.aclose())
|
|
373
|
-
except Exception:
|
|
374
|
-
pass # Ignore cleanup errors
|
|
370
|
+
await self._http_client.aclose()
|
|
371
|
+
self._http_client = None
|
|
375
372
|
|
|
376
373
|
|
|
377
374
|
__all__ = ["CentrifugoAdminAPIViewSet"]
|
|
@@ -5,7 +5,6 @@ Provides endpoints for live testing of Centrifugo integration from dashboard.
|
|
|
5
5
|
Includes connection tokens, publish proxying, and ACK management.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import asyncio
|
|
9
8
|
import time
|
|
10
9
|
from datetime import datetime, timedelta
|
|
11
10
|
from typing import Any, Dict
|
|
@@ -32,23 +31,6 @@ logger = get_logger("centrifugo.testing_api")
|
|
|
32
31
|
# ========================================================================
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
class ConnectionTokenRequest(BaseModel):
|
|
36
|
-
"""Request model for connection token generation."""
|
|
37
|
-
|
|
38
|
-
user_id: str = Field(..., description="User ID for the connection")
|
|
39
|
-
channels: list[str] = Field(
|
|
40
|
-
default_factory=list, description="List of channels to authorize"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class ConnectionTokenResponse(BaseModel):
|
|
45
|
-
"""Response model for connection token."""
|
|
46
|
-
|
|
47
|
-
token: str = Field(..., description="JWT token for WebSocket connection")
|
|
48
|
-
centrifugo_url: str = Field(..., description="Centrifugo WebSocket URL")
|
|
49
|
-
expires_at: str = Field(..., description="Token expiration time (ISO 8601)")
|
|
50
|
-
|
|
51
|
-
|
|
52
34
|
class PublishTestRequest(BaseModel):
|
|
53
35
|
"""Request model for test message publishing."""
|
|
54
36
|
|
|
@@ -132,68 +114,6 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
132
114
|
|
|
133
115
|
return self._http_client
|
|
134
116
|
|
|
135
|
-
@extend_schema(
|
|
136
|
-
tags=["Centrifugo Testing"],
|
|
137
|
-
summary="Generate connection token",
|
|
138
|
-
description="Generate JWT token for WebSocket connection to Centrifugo.",
|
|
139
|
-
request=ConnectionTokenRequest,
|
|
140
|
-
responses={
|
|
141
|
-
200: ConnectionTokenResponse,
|
|
142
|
-
400: {"description": "Invalid request"},
|
|
143
|
-
500: {"description": "Server error"},
|
|
144
|
-
},
|
|
145
|
-
)
|
|
146
|
-
@action(detail=False, methods=["post"], url_path="connection-token")
|
|
147
|
-
def connection_token(self, request):
|
|
148
|
-
"""
|
|
149
|
-
Generate JWT token for WebSocket connection.
|
|
150
|
-
|
|
151
|
-
Returns token that can be used to connect to Centrifugo from browser.
|
|
152
|
-
"""
|
|
153
|
-
try:
|
|
154
|
-
config = get_centrifugo_config()
|
|
155
|
-
if not config:
|
|
156
|
-
return Response(
|
|
157
|
-
{"error": "Centrifugo not configured"},
|
|
158
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Parse request
|
|
162
|
-
req_data = ConnectionTokenRequest(**request.data)
|
|
163
|
-
|
|
164
|
-
# Generate JWT token
|
|
165
|
-
now = int(time.time())
|
|
166
|
-
exp = now + 3600 # 1 hour
|
|
167
|
-
|
|
168
|
-
payload = {
|
|
169
|
-
"sub": req_data.user_id,
|
|
170
|
-
"exp": exp,
|
|
171
|
-
"iat": now,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
# Add channels if provided
|
|
175
|
-
if req_data.channels:
|
|
176
|
-
payload["channels"] = req_data.channels
|
|
177
|
-
|
|
178
|
-
# Use HMAC secret from config or Django SECRET_KEY
|
|
179
|
-
secret = config.centrifugo_token_hmac_secret or settings.SECRET_KEY
|
|
180
|
-
|
|
181
|
-
token = jwt.encode(payload, secret, algorithm="HS256")
|
|
182
|
-
|
|
183
|
-
response = ConnectionTokenResponse(
|
|
184
|
-
token=token,
|
|
185
|
-
centrifugo_url=config.centrifugo_url,
|
|
186
|
-
expires_at=datetime.utcfromtimestamp(exp).isoformat() + "Z",
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
return Response(response.model_dump())
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
logger.error(f"Failed to generate connection token: {e}", exc_info=True)
|
|
193
|
-
return Response(
|
|
194
|
-
{"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
195
|
-
)
|
|
196
|
-
|
|
197
117
|
@extend_schema(
|
|
198
118
|
tags=["Centrifugo Testing"],
|
|
199
119
|
summary="Publish test message",
|
|
@@ -206,23 +126,21 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
206
126
|
},
|
|
207
127
|
)
|
|
208
128
|
@action(detail=False, methods=["post"], url_path="publish-test")
|
|
209
|
-
def publish_test(self, request):
|
|
129
|
+
async def publish_test(self, request):
|
|
210
130
|
"""
|
|
211
|
-
Publish test message via wrapper.
|
|
131
|
+
Publish test message via wrapper (ASYNC).
|
|
212
132
|
|
|
213
133
|
Proxies request to Centrifugo wrapper with ACK tracking support.
|
|
214
134
|
"""
|
|
215
135
|
try:
|
|
216
136
|
req_data = PublishTestRequest(**request.data)
|
|
217
137
|
|
|
218
|
-
# Call wrapper API
|
|
219
|
-
result =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
ack_timeout=req_data.ack_timeout,
|
|
225
|
-
)
|
|
138
|
+
# Call wrapper API (ASYNC - no asyncio.run()!)
|
|
139
|
+
result = await self._publish_to_wrapper(
|
|
140
|
+
channel=req_data.channel,
|
|
141
|
+
data=req_data.data,
|
|
142
|
+
wait_for_ack=req_data.wait_for_ack,
|
|
143
|
+
ack_timeout=req_data.ack_timeout,
|
|
226
144
|
)
|
|
227
145
|
|
|
228
146
|
response = PublishTestResponse(
|
|
@@ -259,20 +177,18 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
259
177
|
},
|
|
260
178
|
)
|
|
261
179
|
@action(detail=False, methods=["post"], url_path="send-ack")
|
|
262
|
-
def send_ack(self, request):
|
|
180
|
+
async def send_ack(self, request):
|
|
263
181
|
"""
|
|
264
|
-
Send manual ACK for message.
|
|
182
|
+
Send manual ACK for message (ASYNC).
|
|
265
183
|
|
|
266
184
|
Proxies ACK to wrapper for testing ACK flow.
|
|
267
185
|
"""
|
|
268
186
|
try:
|
|
269
187
|
req_data = ManualAckRequest(**request.data)
|
|
270
188
|
|
|
271
|
-
# Send ACK to wrapper
|
|
272
|
-
result =
|
|
273
|
-
|
|
274
|
-
message_id=req_data.message_id, client_id=req_data.client_id
|
|
275
|
-
)
|
|
189
|
+
# Send ACK to wrapper (ASYNC - no asyncio.run()!)
|
|
190
|
+
result = await self._send_ack_to_wrapper(
|
|
191
|
+
message_id=req_data.message_id, client_id=req_data.client_id
|
|
276
192
|
)
|
|
277
193
|
|
|
278
194
|
response = ManualAckResponse(
|
|
@@ -413,9 +329,9 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
413
329
|
},
|
|
414
330
|
)
|
|
415
331
|
@action(detail=False, methods=["post"], url_path="publish-with-logging")
|
|
416
|
-
def publish_with_logging(self, request):
|
|
332
|
+
async def publish_with_logging(self, request):
|
|
417
333
|
"""
|
|
418
|
-
Publish message using CentrifugoClient with database logging.
|
|
334
|
+
Publish message using CentrifugoClient with database logging (ASYNC).
|
|
419
335
|
|
|
420
336
|
This endpoint uses the production CentrifugoClient which logs all
|
|
421
337
|
publishes to the database (CentrifugoLog model).
|
|
@@ -426,25 +342,24 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
426
342
|
# Use CentrifugoClient for publishing
|
|
427
343
|
client = CentrifugoClient()
|
|
428
344
|
|
|
429
|
-
# Publish message
|
|
430
|
-
|
|
431
|
-
client.publish_with_ack(
|
|
345
|
+
# Publish message (ASYNC - no asyncio.run()!)
|
|
346
|
+
if req_data.wait_for_ack:
|
|
347
|
+
result = await client.publish_with_ack(
|
|
432
348
|
channel=req_data.channel,
|
|
433
349
|
data=req_data.data,
|
|
434
|
-
ack_timeout=req_data.ack_timeout
|
|
350
|
+
ack_timeout=req_data.ack_timeout,
|
|
435
351
|
user=request.user if request.user.is_authenticated else None,
|
|
436
352
|
caller_ip=request.META.get("REMOTE_ADDR"),
|
|
437
353
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
438
354
|
)
|
|
439
|
-
|
|
440
|
-
|
|
355
|
+
else:
|
|
356
|
+
result = await client.publish(
|
|
441
357
|
channel=req_data.channel,
|
|
442
358
|
data=req_data.data,
|
|
443
359
|
user=request.user if request.user.is_authenticated else None,
|
|
444
360
|
caller_ip=request.META.get("REMOTE_ADDR"),
|
|
445
361
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
446
362
|
)
|
|
447
|
-
)
|
|
448
363
|
|
|
449
364
|
# Convert PublishResponse to dict
|
|
450
365
|
response_data = {
|
|
@@ -465,17 +380,17 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
465
380
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
466
381
|
)
|
|
467
382
|
|
|
468
|
-
def
|
|
469
|
-
"""
|
|
383
|
+
async def cleanup(self):
|
|
384
|
+
"""
|
|
385
|
+
Explicit async cleanup method for HTTP client.
|
|
386
|
+
|
|
387
|
+
Note: Django handles ViewSet lifecycle automatically.
|
|
388
|
+
This method is provided for explicit cleanup if needed,
|
|
389
|
+
but httpx.AsyncClient will be garbage collected normally.
|
|
390
|
+
"""
|
|
470
391
|
if self._http_client:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if loop.is_running():
|
|
474
|
-
loop.create_task(self._http_client.aclose())
|
|
475
|
-
else:
|
|
476
|
-
loop.run_until_complete(self._http_client.aclose())
|
|
477
|
-
except Exception:
|
|
478
|
-
pass
|
|
392
|
+
await self._http_client.aclose()
|
|
393
|
+
self._http_client = None
|
|
479
394
|
|
|
480
395
|
|
|
481
396
|
__all__ = ["CentrifugoTestingAPIViewSet"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Token API
|
|
3
|
+
|
|
4
|
+
Provides endpoint for generating Centrifugo JWT tokens with user permissions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from drf_spectacular.utils import extend_schema
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from rest_framework import status, viewsets
|
|
12
|
+
from rest_framework.decorators import action
|
|
13
|
+
from rest_framework.response import Response
|
|
14
|
+
from rest_framework.permissions import IsAuthenticated
|
|
15
|
+
|
|
16
|
+
from ..services import generate_centrifugo_token
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ========================================================================
|
|
22
|
+
# Response Models
|
|
23
|
+
# ========================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConnectionTokenResponse(BaseModel):
|
|
27
|
+
"""Response model for Centrifugo connection token."""
|
|
28
|
+
|
|
29
|
+
token: str = Field(..., description="JWT token for Centrifugo connection")
|
|
30
|
+
centrifugo_url: str = Field(..., description="Centrifugo WebSocket URL")
|
|
31
|
+
expires_at: str = Field(..., description="Token expiration time (ISO 8601)")
|
|
32
|
+
channels: list[str] = Field(..., description="List of allowed channels")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CentrifugoTokenViewSet(viewsets.ViewSet):
|
|
36
|
+
"""
|
|
37
|
+
Centrifugo Token API ViewSet.
|
|
38
|
+
|
|
39
|
+
Provides endpoint for authenticated users to get Centrifugo JWT token
|
|
40
|
+
with their allowed channels based on permissions.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
permission_classes = [IsAuthenticated]
|
|
44
|
+
|
|
45
|
+
@extend_schema(
|
|
46
|
+
tags=["Centrifugo Auth"],
|
|
47
|
+
summary="Get Centrifugo connection token",
|
|
48
|
+
description=(
|
|
49
|
+
"Generate JWT token for WebSocket connection to Centrifugo. "
|
|
50
|
+
"Token includes user's allowed channels based on their permissions. "
|
|
51
|
+
"Requires authentication."
|
|
52
|
+
),
|
|
53
|
+
responses={
|
|
54
|
+
200: ConnectionTokenResponse,
|
|
55
|
+
401: {"description": "Unauthorized - authentication required"},
|
|
56
|
+
500: {"description": "Server error"},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
@action(detail=False, methods=["get"], url_path="token")
|
|
60
|
+
def get_token(self, request):
|
|
61
|
+
"""
|
|
62
|
+
Get Centrifugo connection token for authenticated user.
|
|
63
|
+
|
|
64
|
+
Returns JWT token with user's allowed channels that can be used
|
|
65
|
+
to connect to Centrifugo WebSocket.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
user = request.user
|
|
69
|
+
|
|
70
|
+
# Generate token with user's channels
|
|
71
|
+
token_data = generate_centrifugo_token(user)
|
|
72
|
+
|
|
73
|
+
logger.debug(
|
|
74
|
+
f"Generated Centrifugo token for user {user.email} (ID: {user.id}) "
|
|
75
|
+
f"with {len(token_data['channels'])} channels: {token_data['channels']}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Format expires_at as ISO 8601 string
|
|
79
|
+
return Response({
|
|
80
|
+
"token": token_data["token"],
|
|
81
|
+
"centrifugo_url": token_data["centrifugo_url"],
|
|
82
|
+
"expires_at": token_data["expires_at"].isoformat() + "Z",
|
|
83
|
+
"channels": token_data["channels"],
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
except ValueError as e:
|
|
87
|
+
# Centrifugo not configured or disabled
|
|
88
|
+
logger.warning(f"Centrifugo token generation failed: {e}")
|
|
89
|
+
return Response(
|
|
90
|
+
{"error": str(e)},
|
|
91
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
92
|
+
)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Failed to generate Centrifugo token: {e}", exc_info=True)
|
|
95
|
+
return Response(
|
|
96
|
+
{"error": "Failed to generate token"},
|
|
97
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = ["CentrifugoTokenViewSet"]
|