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.
- 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/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/direct_client.py +282 -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/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +55 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +311 -7
- 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/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +206 -5
- 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/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -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/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/RECORD +53 -37
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {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",
|