django-cfg 1.5.14__py3-none-any.whl → 1.5.20__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.

Files changed (53) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  3. django_cfg/apps/business/support/serializers.py +3 -2
  4. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  5. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  6. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
  7. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  8. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  9. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  10. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  11. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  12. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  13. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  14. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  15. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  16. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  17. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  18. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  19. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  20. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  21. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  22. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  23. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  24. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  25. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  26. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +55 -0
  27. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +311 -7
  28. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  29. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  30. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  31. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  32. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +206 -5
  33. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  34. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  35. django_cfg/apps/system/frontend/views.py +87 -6
  36. django_cfg/core/builders/security_builder.py +1 -0
  37. django_cfg/core/generation/integration_generators/api.py +2 -0
  38. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  39. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  40. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  41. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  42. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  43. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  44. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  45. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  46. django_cfg/modules/django_client/core/parser/base.py +12 -0
  47. django_cfg/pyproject.toml +1 -1
  48. django_cfg/static/frontend/admin.zip +0 -0
  49. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
  50. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/RECORD +53 -37
  51. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  52. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  53. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,371 @@
1
+ """
2
+ Centrifugo Publishing Service.
3
+
4
+ Unified high-level API for publishing events to Centrifugo.
5
+ Abstracts away CentrifugoClient details and provides domain-specific methods.
6
+
7
+ Usage:
8
+ >>> from django_cfg.apps.integrations.centrifugo.services import CentrifugoPublisher
9
+ >>>
10
+ >>> publisher = CentrifugoPublisher()
11
+ >>>
12
+ >>> # Publish gRPC event
13
+ >>> await publisher.publish_grpc_event(
14
+ ... channel="grpc#bot#123#status",
15
+ ... method="/bot.BotService/Start",
16
+ ... status="OK",
17
+ ... duration_ms=150
18
+ ... )
19
+ >>>
20
+ >>> # Publish demo event
21
+ >>> await publisher.publish_demo_event(
22
+ ... channel="grpc#demo#test",
23
+ ... metadata={"test": True}
24
+ ... )
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from datetime import datetime, timezone as tz
30
+ from typing import Any, Dict, Optional
31
+
32
+ from django_cfg.modules.django_logging import get_logger
33
+
34
+ from ..services.client import (
35
+ CentrifugoClient,
36
+ DirectCentrifugoClient,
37
+ PublishResponse,
38
+ get_centrifugo_client,
39
+ get_direct_centrifugo_client,
40
+ )
41
+
42
+ logger = get_logger("centrifugo.publisher")
43
+
44
+
45
+ class CentrifugoPublisher:
46
+ """
47
+ High-level publishing service for Centrifugo events.
48
+
49
+ Provides domain-specific methods that abstract away low-level client details.
50
+ All methods are async and handle errors gracefully.
51
+
52
+ Features:
53
+ - Unified API for all Centrifugo publishing
54
+ - Automatic timestamp injection
55
+ - Type-safe event metadata
56
+ - Error handling and logging
57
+ - Easy to mock for testing
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ client: Optional[CentrifugoClient | DirectCentrifugoClient] = None,
63
+ use_direct: bool = True,
64
+ ):
65
+ """
66
+ Initialize publisher.
67
+
68
+ Args:
69
+ client: Optional client instance (CentrifugoClient or DirectCentrifugoClient)
70
+ use_direct: Use DirectCentrifugoClient (bypass wrapper, default=True)
71
+ """
72
+ if client:
73
+ self._client = client
74
+ logger.debug("CentrifugoPublisher initialized with custom client")
75
+ elif use_direct:
76
+ # Use direct client (no wrapper, no DB logging)
77
+ self._client = get_direct_centrifugo_client()
78
+ logger.debug("CentrifugoPublisher initialized with DirectCentrifugoClient")
79
+ else:
80
+ # Use wrapper client (with auth & DB logging)
81
+ self._client = get_centrifugo_client()
82
+ logger.debug("CentrifugoPublisher initialized with CentrifugoClient (wrapper)")
83
+
84
+ @property
85
+ def client(self) -> CentrifugoClient | DirectCentrifugoClient:
86
+ """Get underlying client instance."""
87
+ return self._client
88
+
89
+ async def publish_grpc_event(
90
+ self,
91
+ channel: str,
92
+ method: str,
93
+ status: str = "OK",
94
+ duration_ms: float = 0.0,
95
+ peer: Optional[str] = None,
96
+ metadata: Optional[Dict[str, Any]] = None,
97
+ **extra: Any,
98
+ ) -> PublishResponse:
99
+ """
100
+ Publish gRPC event (interceptor-style metadata).
101
+
102
+ Args:
103
+ channel: Centrifugo channel (e.g., "grpc#bot#123#status")
104
+ method: Full gRPC method name (e.g., "/bot.BotService/Start")
105
+ status: RPC status code (default: "OK")
106
+ duration_ms: RPC duration in milliseconds
107
+ peer: Client peer address
108
+ metadata: Additional metadata dict
109
+ **extra: Additional fields
110
+
111
+ Returns:
112
+ PublishResponse with result
113
+
114
+ Example:
115
+ >>> await publisher.publish_grpc_event(
116
+ ... channel="grpc#bot#123#status",
117
+ ... method="/bot.BotService/Start",
118
+ ... status="OK",
119
+ ... duration_ms=150,
120
+ ... peer="127.0.0.1:50051"
121
+ ... )
122
+ """
123
+ # Parse method name
124
+ service_name = None
125
+ method_name = None
126
+ if method.startswith("/") and "/" in method[1:]:
127
+ parts = method[1:].split("/")
128
+ service_name = parts[0]
129
+ method_name = parts[1]
130
+
131
+ # Build event data
132
+ event_data = {
133
+ "event_type": "grpc_event",
134
+ "method": method,
135
+ "status": status,
136
+ "timestamp": datetime.now(tz.utc).isoformat(),
137
+ }
138
+
139
+ if service_name:
140
+ event_data["service"] = service_name
141
+ if method_name:
142
+ event_data["method_name"] = method_name
143
+ if duration_ms:
144
+ event_data["duration_ms"] = duration_ms
145
+ if peer:
146
+ event_data["peer"] = peer
147
+ if metadata:
148
+ event_data.update(metadata)
149
+ if extra:
150
+ event_data.update(extra)
151
+
152
+ logger.debug(f"Publishing gRPC event: {channel} ({method})")
153
+
154
+ # DirectCentrifugoClient uses simpler API
155
+ if isinstance(self._client, DirectCentrifugoClient):
156
+ return await self._client.publish(channel=channel, data=event_data)
157
+ else:
158
+ return await self._client.publish(channel=channel, data=event_data)
159
+
160
+ async def publish_demo_event(
161
+ self,
162
+ channel: str,
163
+ metadata: Optional[Dict[str, Any]] = None,
164
+ **extra: Any,
165
+ ) -> PublishResponse:
166
+ """
167
+ Publish demo/test event.
168
+
169
+ Args:
170
+ channel: Centrifugo channel
171
+ metadata: Event metadata
172
+ **extra: Additional fields
173
+
174
+ Returns:
175
+ PublishResponse with result
176
+
177
+ Example:
178
+ >>> await publisher.publish_demo_event(
179
+ ... channel="grpc#demo#test",
180
+ ... metadata={"test": True, "source": "demo.py"}
181
+ ... )
182
+ """
183
+ event_data = {
184
+ "event_type": "demo_event",
185
+ "timestamp": datetime.now(tz.utc).isoformat(),
186
+ "test_mode": True,
187
+ }
188
+
189
+ if metadata:
190
+ event_data.update(metadata)
191
+ if extra:
192
+ event_data.update(extra)
193
+
194
+ logger.debug(f"Publishing demo event: {channel}")
195
+
196
+ if isinstance(self._client, DirectCentrifugoClient):
197
+ return await self._client.publish(channel=channel, data=event_data)
198
+ else:
199
+ return await self._client.publish(channel=channel, data=event_data)
200
+
201
+ async def publish_notification(
202
+ self,
203
+ channel: str,
204
+ title: str,
205
+ message: str,
206
+ level: str = "info",
207
+ user: Optional[Any] = None,
208
+ metadata: Optional[Dict[str, Any]] = None,
209
+ **extra: Any,
210
+ ) -> PublishResponse:
211
+ """
212
+ Publish user notification.
213
+
214
+ Args:
215
+ channel: Centrifugo channel (e.g., "notifications#user#123")
216
+ title: Notification title
217
+ message: Notification message
218
+ level: Notification level (info, warning, error, success)
219
+ user: Django User instance
220
+ metadata: Additional metadata
221
+ **extra: Additional fields
222
+
223
+ Returns:
224
+ PublishResponse with result
225
+
226
+ Example:
227
+ >>> await publisher.publish_notification(
228
+ ... channel="notifications#user#123",
229
+ ... title="Bot Started",
230
+ ... message="Your bot has started successfully",
231
+ ... level="success"
232
+ ... )
233
+ """
234
+ event_data = {
235
+ "event_type": "notification",
236
+ "title": title,
237
+ "message": message,
238
+ "level": level,
239
+ "timestamp": datetime.now(tz.utc).isoformat(),
240
+ }
241
+
242
+ if metadata:
243
+ event_data.update(metadata)
244
+ if extra:
245
+ event_data.update(extra)
246
+
247
+ logger.debug(f"Publishing notification: {channel} ({title})")
248
+
249
+ return await self._client.publish(channel=channel, data=event_data, user=user)
250
+
251
+ async def publish_status_change(
252
+ self,
253
+ channel: str,
254
+ old_status: str,
255
+ new_status: str,
256
+ reason: Optional[str] = None,
257
+ metadata: Optional[Dict[str, Any]] = None,
258
+ **extra: Any,
259
+ ) -> PublishResponse:
260
+ """
261
+ Publish status change event.
262
+
263
+ Args:
264
+ channel: Centrifugo channel
265
+ old_status: Previous status
266
+ new_status: New status
267
+ reason: Reason for status change
268
+ metadata: Additional metadata
269
+ **extra: Additional fields
270
+
271
+ Returns:
272
+ PublishResponse with result
273
+
274
+ Example:
275
+ >>> await publisher.publish_status_change(
276
+ ... channel="bot#123#status",
277
+ ... old_status="STOPPED",
278
+ ... new_status="RUNNING",
279
+ ... reason="User requested start"
280
+ ... )
281
+ """
282
+ event_data = {
283
+ "event_type": "status_change",
284
+ "old_status": old_status,
285
+ "new_status": new_status,
286
+ "timestamp": datetime.now(tz.utc).isoformat(),
287
+ }
288
+
289
+ if reason:
290
+ event_data["reason"] = reason
291
+ if metadata:
292
+ event_data.update(metadata)
293
+ if extra:
294
+ event_data.update(extra)
295
+
296
+ logger.debug(f"Publishing status change: {channel} ({old_status} → {new_status})")
297
+
298
+ return await self._client.publish(channel=channel, data=event_data)
299
+
300
+ async def publish_custom(
301
+ self,
302
+ channel: str,
303
+ event_type: str,
304
+ data: Dict[str, Any],
305
+ user: Optional[Any] = None,
306
+ ) -> PublishResponse:
307
+ """
308
+ Publish custom event with arbitrary data.
309
+
310
+ Args:
311
+ channel: Centrifugo channel
312
+ event_type: Custom event type
313
+ data: Event data dict
314
+ user: Django User instance
315
+
316
+ Returns:
317
+ PublishResponse with result
318
+
319
+ Example:
320
+ >>> await publisher.publish_custom(
321
+ ... channel="custom#events",
322
+ ... event_type="custom_event",
323
+ ... data={"foo": "bar", "count": 42}
324
+ ... )
325
+ """
326
+ event_data = {
327
+ "event_type": event_type,
328
+ "timestamp": datetime.now(tz.utc).isoformat(),
329
+ **data,
330
+ }
331
+
332
+ logger.debug(f"Publishing custom event: {channel} ({event_type})")
333
+
334
+ return await self._client.publish(channel=channel, data=event_data, user=user)
335
+
336
+
337
+ # Singleton instance
338
+ _publisher_instance: Optional[CentrifugoPublisher] = None
339
+
340
+
341
+ def get_centrifugo_publisher(client: Optional[CentrifugoClient] = None) -> CentrifugoPublisher:
342
+ """
343
+ Get singleton CentrifugoPublisher instance.
344
+
345
+ Args:
346
+ client: Optional CentrifugoClient (creates new publisher if provided)
347
+
348
+ Returns:
349
+ CentrifugoPublisher instance
350
+
351
+ Example:
352
+ >>> from django_cfg.apps.integrations.centrifugo.services import get_centrifugo_publisher
353
+ >>> publisher = get_centrifugo_publisher()
354
+ >>> await publisher.publish_demo_event(channel="test", metadata={"foo": "bar"})
355
+ """
356
+ global _publisher_instance
357
+
358
+ if client is not None:
359
+ # Create new instance with custom client
360
+ return CentrifugoPublisher(client=client)
361
+
362
+ if _publisher_instance is None:
363
+ _publisher_instance = CentrifugoPublisher()
364
+
365
+ return _publisher_instance
366
+
367
+
368
+ __all__ = [
369
+ "CentrifugoPublisher",
370
+ "get_centrifugo_publisher",
371
+ ]
@@ -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
  ]
@@ -32,23 +32,6 @@ logger = get_logger("centrifugo.testing_api")
32
32
  # ========================================================================
33
33
 
34
34
 
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
35
  class PublishTestRequest(BaseModel):
53
36
  """Request model for test message publishing."""
54
37
 
@@ -132,68 +115,6 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
132
115
 
133
116
  return self._http_client
134
117
 
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
118
  @extend_schema(
198
119
  tags=["Centrifugo Testing"],
199
120
  summary="Publish test message",