django-cfg 1.5.8__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 (159) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  6. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  7. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  8. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  10. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  11. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  12. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  14. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  15. django_cfg/apps/business/support/serializers.py +3 -2
  16. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  17. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  18. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
  19. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  20. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  21. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  22. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  23. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  24. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  25. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  26. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  27. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  28. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  29. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  30. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  31. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  32. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  33. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  34. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  35. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  36. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  37. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  38. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  39. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  40. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  41. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  42. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  43. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  44. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  45. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  46. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  47. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  48. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  49. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  50. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  51. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
  52. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
  53. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  54. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  55. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  56. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  57. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  58. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  59. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  60. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  61. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  62. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  63. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  64. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  65. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  66. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  67. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  68. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  69. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  70. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  71. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  72. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  73. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  74. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  75. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  76. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  77. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  78. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  79. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  80. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  81. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  82. django_cfg/apps/integrations/grpc/urls.py +8 -0
  83. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  84. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  85. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  86. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  87. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
  88. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  89. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  90. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  91. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  92. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  93. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  94. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  95. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  96. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  97. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  98. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  99. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  100. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  101. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  102. django_cfg/apps/system/frontend/views.py +87 -6
  103. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  104. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  105. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  106. django_cfg/config.py +33 -0
  107. django_cfg/core/builders/security_builder.py +1 -0
  108. django_cfg/core/generation/integration_generators/api.py +2 -0
  109. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  110. django_cfg/management/commands/check_endpoints.py +2 -2
  111. django_cfg/management/commands/check_settings.py +3 -10
  112. django_cfg/management/commands/clear_constance.py +3 -10
  113. django_cfg/management/commands/create_token.py +4 -11
  114. django_cfg/management/commands/list_urls.py +4 -10
  115. django_cfg/management/commands/migrate_all.py +18 -12
  116. django_cfg/management/commands/migrator.py +4 -11
  117. django_cfg/management/commands/script.py +4 -10
  118. django_cfg/management/commands/show_config.py +8 -16
  119. django_cfg/management/commands/show_urls.py +5 -11
  120. django_cfg/management/commands/superuser.py +4 -11
  121. django_cfg/management/commands/tree.py +5 -10
  122. django_cfg/management/utils/README.md +402 -0
  123. django_cfg/management/utils/__init__.py +29 -0
  124. django_cfg/management/utils/mixins.py +176 -0
  125. django_cfg/middleware/pagination.py +53 -54
  126. django_cfg/models/api/grpc/__init__.py +15 -21
  127. django_cfg/models/api/grpc/config.py +155 -73
  128. django_cfg/models/ngrok/config.py +7 -6
  129. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  130. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  131. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  132. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  133. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  134. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  135. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  136. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  137. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  138. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  139. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  140. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  141. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  142. django_cfg/modules/django_client/core/parser/base.py +126 -30
  143. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  144. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  145. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  146. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  147. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  148. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  149. django_cfg/modules/django_unfold/navigation.py +6 -18
  150. django_cfg/pyproject.toml +1 -1
  151. django_cfg/registry/modules.py +1 -4
  152. django_cfg/requirements.txt +52 -0
  153. django_cfg/static/frontend/admin.zip +0 -0
  154. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
  155. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
  156. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  157. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  158. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  159. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
@@ -85,8 +85,9 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
85
85
  "total_examples": len(examples),
86
86
  }
87
87
 
88
- serializer = GRPCExamplesListSerializer(**response_data)
89
- return Response(serializer.model_dump())
88
+ serializer = GRPCExamplesListSerializer(data=response_data)
89
+ serializer.is_valid(raise_exception=True)
90
+ return Response(serializer.data)
90
91
 
91
92
  except Exception as e:
92
93
  logger.error(f"Examples fetch error: {e}", exc_info=True)
@@ -147,35 +148,33 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
147
148
  # Serialize paginated data
148
149
  logs_list = []
149
150
  for log in page:
150
- log_serializer = GRPCTestLogSerializer(
151
- request_id=log.request_id,
152
- service=log.service_name,
153
- method=log.method_name,
154
- status=log.status,
155
- grpc_status_code=log.grpc_status_code or "",
156
- error_message=log.error_message or "",
157
- duration_ms=log.duration_ms or 0,
158
- created_at=log.created_at.isoformat(),
159
- user=log.user.username if log.user else None,
160
- )
161
- logs_list.append(log_serializer.model_dump())
151
+ logs_list.append({
152
+ "request_id": log.request_id,
153
+ "service": log.service_name,
154
+ "method": log.method_name,
155
+ "status": log.status,
156
+ "grpc_status_code": log.grpc_status_code or "",
157
+ "error_message": log.error_message or "",
158
+ "duration_ms": log.duration_ms or 0,
159
+ "created_at": log.created_at.isoformat(),
160
+ "user": log.user.username if log.user else None,
161
+ })
162
162
  return self.get_paginated_response(logs_list)
163
163
 
164
164
  # No pagination (shouldn't happen with default pagination)
165
165
  logs_list = []
166
166
  for log in queryset[:100]: # Safety limit
167
- log_serializer = GRPCTestLogSerializer(
168
- request_id=log.request_id,
169
- service=log.service_name,
170
- method=log.method_name,
171
- status=log.status,
172
- grpc_status_code=log.grpc_status_code or "",
173
- error_message=log.error_message or "",
174
- duration_ms=log.duration_ms or 0,
175
- created_at=log.created_at.isoformat(),
176
- user=log.user.username if log.user else None,
177
- )
178
- logs_list.append(log_serializer.model_dump())
167
+ logs_list.append({
168
+ "request_id": log.request_id,
169
+ "service": log.service_name,
170
+ "method": log.method_name,
171
+ "status": log.status,
172
+ "grpc_status_code": log.grpc_status_code or "",
173
+ "error_message": log.error_message or "",
174
+ "duration_ms": log.duration_ms or 0,
175
+ "created_at": log.created_at.isoformat(),
176
+ "user": log.user.username if log.user else None,
177
+ })
179
178
  return Response({"logs": logs_list, "count": len(logs_list)})
180
179
 
181
180
  except Exception as e:
@@ -221,9 +220,10 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
221
220
  from django.utils import timezone
222
221
 
223
222
  try:
224
- # Validate request - Pydantic validation
225
- serializer = GRPCCallRequestSerializer(**request.data)
226
- data = serializer.model_dump()
223
+ # Validate request - DRF validation
224
+ serializer = GRPCCallRequestSerializer(data=request.data)
225
+ serializer.is_valid(raise_exception=True)
226
+ data = serializer.validated_data
227
227
 
228
228
  service_name = data["service"]
229
229
  method_name = data["method"]
@@ -275,21 +275,23 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
275
275
  if response_data and isinstance(response_data, dict):
276
276
  response_data = json.dumps(response_data)
277
277
 
278
- response_serializer = GRPCCallResponseSerializer(
279
- success=result["success"],
280
- request_id=request_id,
281
- service=result["service"],
282
- method=result["method"],
283
- status=log.status,
284
- grpc_status_code=result["grpc_status_code"],
285
- duration_ms=result["duration_ms"],
286
- response=response_data,
287
- error=result.get("error_message"),
288
- metadata={},
289
- timestamp=end_time.isoformat(),
290
- )
278
+ response_data_dict = {
279
+ "success": result["success"],
280
+ "request_id": request_id,
281
+ "service": result["service"],
282
+ "method": result["method"],
283
+ "status": log.status,
284
+ "grpc_status_code": result["grpc_status_code"],
285
+ "duration_ms": result["duration_ms"],
286
+ "response": response_data,
287
+ "error": result.get("error_message"),
288
+ "metadata": {},
289
+ "timestamp": end_time.isoformat(),
290
+ }
291
291
 
292
- return Response(response_serializer.model_dump(), status=status.HTTP_200_OK)
292
+ response_serializer = GRPCCallResponseSerializer(data=response_data_dict)
293
+ response_serializer.is_valid(raise_exception=True)
294
+ return Response(response_serializer.data, status=status.HTTP_200_OK)
293
295
 
294
296
  except Exception as e:
295
297
  logger.error(f"gRPC call endpoint error: {e}", exc_info=True)
@@ -19,7 +19,7 @@ from ..services import job_to_model
19
19
  logger = get_logger("rq.jobs")
20
20
 
21
21
 
22
- class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
22
+ class JobViewSet(AdminAPIMixin, viewsets.GenericViewSet):
23
23
  """
24
24
  ViewSet for RQ job management.
25
25
 
@@ -33,6 +33,8 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
33
33
  Requires admin authentication (JWT, Session, or Basic Auth).
34
34
  """
35
35
 
36
+ serializer_class = JobListSerializer
37
+
36
38
  @extend_schema(
37
39
  tags=["RQ Jobs"],
38
40
  summary="List all jobs",
@@ -455,9 +457,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
455
457
  except Exception as e:
456
458
  logger.debug(f"Failed to get failed jobs for queue {queue_name}: {e}")
457
459
 
458
- serializer = JobListSerializer(data=all_jobs, many=True)
460
+ # Use DRF pagination
461
+ page = self.paginate_queryset(all_jobs)
462
+ serializer = JobListSerializer(data=page, many=True)
459
463
  serializer.is_valid(raise_exception=True)
460
- return Response(serializer.data)
464
+ return self.get_paginated_response(serializer.data)
461
465
 
462
466
  except Exception as e:
463
467
  logger.error(f"Failed jobs list error: {e}", exc_info=True)
@@ -530,9 +534,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
530
534
  except Exception as e:
531
535
  logger.debug(f"Failed to get finished jobs for queue {queue_name}: {e}")
532
536
 
533
- serializer = JobListSerializer(data=all_jobs, many=True)
537
+ # Use DRF pagination
538
+ page = self.paginate_queryset(all_jobs)
539
+ serializer = JobListSerializer(data=page, many=True)
534
540
  serializer.is_valid(raise_exception=True)
535
- return Response(serializer.data)
541
+ return self.get_paginated_response(serializer.data)
536
542
 
537
543
  except Exception as e:
538
544
  logger.error(f"Finished jobs list error: {e}", exc_info=True)
@@ -795,9 +801,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
795
801
  except Exception as e:
796
802
  logger.debug(f"Failed to get deferred jobs for queue {queue_name}: {e}")
797
803
 
798
- serializer = JobListSerializer(data=all_jobs, many=True)
804
+ # Use DRF pagination
805
+ page = self.paginate_queryset(all_jobs)
806
+ serializer = JobListSerializer(data=page, many=True)
799
807
  serializer.is_valid(raise_exception=True)
800
- return Response(serializer.data)
808
+ return self.get_paginated_response(serializer.data)
801
809
 
802
810
  except Exception as e:
803
811
  logger.error(f"Deferred jobs list error: {e}", exc_info=True)
@@ -870,9 +878,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
870
878
  except Exception as e:
871
879
  logger.debug(f"Failed to get started jobs for queue {queue_name}: {e}")
872
880
 
873
- serializer = JobListSerializer(data=all_jobs, many=True)
881
+ # Use DRF pagination
882
+ page = self.paginate_queryset(all_jobs)
883
+ serializer = JobListSerializer(data=page, many=True)
874
884
  serializer.is_valid(raise_exception=True)
875
- return Response(serializer.data)
885
+ return self.get_paginated_response(serializer.data)
876
886
 
877
887
  except Exception as e:
878
888
  logger.error(f"Started jobs list error: {e}", exc_info=True)
@@ -23,7 +23,7 @@ from ..serializers import (
23
23
  logger = get_logger("rq.schedule")
24
24
 
25
25
 
26
- class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
26
+ class ScheduleViewSet(AdminAPIMixin, viewsets.GenericViewSet):
27
27
  """
28
28
  ViewSet for RQ schedule management.
29
29
 
@@ -37,6 +37,8 @@ class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
37
37
  Requires rq-scheduler to be installed: pip install rq-scheduler
38
38
  """
39
39
 
40
+ serializer_class = ScheduledJobSerializer
41
+
40
42
  @extend_schema(
41
43
  tags=["RQ Schedules"],
42
44
  summary="List scheduled jobs",
@@ -131,9 +133,11 @@ class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
131
133
  }
132
134
  all_jobs.append(job_data)
133
135
 
134
- serializer = ScheduledJobSerializer(data=all_jobs, many=True)
136
+ # Use DRF pagination
137
+ page = self.paginate_queryset(all_jobs)
138
+ serializer = ScheduledJobSerializer(data=page, many=True)
135
139
  serializer.is_valid(raise_exception=True)
136
- return Response(serializer.data)
140
+ return self.get_paginated_response(serializer.data)
137
141
 
138
142
  except Exception as e:
139
143
  logger.error(f"Failed to list scheduled jobs: {e}", exc_info=True)
@@ -8,7 +8,14 @@ from rest_framework import serializers
8
8
 
9
9
 
10
10
  class CommandSerializer(serializers.Serializer):
11
- """Django management command serializer."""
11
+ """
12
+ Django management command serializer.
13
+
14
+ Includes security metadata from base classes (SafeCommand, InteractiveCommand, etc.):
15
+ - web_executable: Can be executed via web interface
16
+ - requires_input: Requires interactive user input
17
+ - is_destructive: Modifies or deletes data
18
+ """
12
19
  name = serializers.CharField()
13
20
  app = serializers.CharField()
14
21
  help = serializers.CharField()
@@ -17,6 +24,23 @@ class CommandSerializer(serializers.Serializer):
17
24
  is_allowed = serializers.BooleanField(required=False)
18
25
  risk_level = serializers.CharField(required=False)
19
26
 
27
+ # Security metadata from command base classes
28
+ web_executable = serializers.BooleanField(
29
+ required=False,
30
+ allow_null=True,
31
+ help_text="Can be executed via web interface"
32
+ )
33
+ requires_input = serializers.BooleanField(
34
+ required=False,
35
+ allow_null=True,
36
+ help_text="Requires interactive user input"
37
+ )
38
+ is_destructive = serializers.BooleanField(
39
+ required=False,
40
+ allow_null=True,
41
+ help_text="Modifies or deletes data"
42
+ )
43
+
20
44
 
21
45
  class CommandsSummarySerializer(serializers.Serializer):
22
46
  """Commands summary serializer."""
@@ -5,6 +5,8 @@ Serializers for displaying user's DjangoConfig settings.
5
5
  """
6
6
 
7
7
  from rest_framework import serializers
8
+ from drf_spectacular.utils import extend_schema_field
9
+ from drf_spectacular.types import OpenApiTypes
8
10
 
9
11
 
10
12
  # Nested serializers for complex structures
@@ -89,6 +91,18 @@ class RedisQueueConfigSerializer(serializers.Serializer):
89
91
  socket_timeout = serializers.IntegerField(required=False, allow_null=True)
90
92
 
91
93
 
94
+ class RQScheduleSerializer(serializers.Serializer):
95
+ """Redis Queue schedule configuration."""
96
+ func = serializers.CharField(required=False, allow_null=True)
97
+ cron_string = serializers.CharField(required=False, allow_null=True)
98
+ queue = serializers.CharField(required=False, allow_null=True)
99
+ kwargs = serializers.DictField(required=False, allow_null=True)
100
+ args = serializers.ListField(required=False, allow_null=True)
101
+ meta = serializers.DictField(required=False, allow_null=True)
102
+ repeat = serializers.IntegerField(required=False, allow_null=True)
103
+ result_ttl = serializers.IntegerField(required=False, allow_null=True)
104
+
105
+
92
106
  class DjangoRQConfigSerializer(serializers.Serializer):
93
107
  """Django-RQ configuration."""
94
108
  enabled = serializers.BooleanField(required=False, allow_null=True)
@@ -97,7 +111,7 @@ class DjangoRQConfigSerializer(serializers.Serializer):
97
111
  exception_handlers = serializers.ListField(required=False, allow_null=True)
98
112
  api_token = serializers.CharField(required=False, allow_null=True)
99
113
  prometheus_enabled = serializers.BooleanField(required=False, allow_null=True)
100
- schedules = serializers.ListField(child=serializers.DictField(), required=False, allow_null=True)
114
+ schedules = serializers.ListField(child=RQScheduleSerializer(), required=False, allow_null=True)
101
115
 
102
116
 
103
117
  class DRFConfigSerializer(serializers.Serializer):
@@ -126,6 +140,78 @@ class ConfigMetaSerializer(serializers.Serializer):
126
140
  secret_key_configured = serializers.BooleanField()
127
141
 
128
142
 
143
+ class TelegramConfigSerializer(serializers.Serializer):
144
+ """Telegram service configuration."""
145
+ bot_token = serializers.CharField(required=False, allow_null=True)
146
+ chat_id = serializers.IntegerField(required=False, allow_null=True)
147
+ parse_mode = serializers.CharField(required=False, allow_null=True)
148
+ disable_notification = serializers.BooleanField(required=False, allow_null=True)
149
+ disable_web_page_preview = serializers.BooleanField(required=False, allow_null=True)
150
+ timeout = serializers.IntegerField(required=False, allow_null=True)
151
+ webhook_url = serializers.CharField(required=False, allow_null=True)
152
+ webhook_secret = serializers.CharField(required=False, allow_null=True)
153
+ max_retries = serializers.IntegerField(required=False, allow_null=True)
154
+ retry_delay = serializers.FloatField(required=False, allow_null=True)
155
+
156
+
157
+ class NgrokConfigSerializer(serializers.Serializer):
158
+ """Ngrok tunneling configuration."""
159
+ enabled = serializers.BooleanField(required=False, allow_null=True)
160
+ authtoken = serializers.CharField(required=False, allow_null=True)
161
+ basic_auth = serializers.ListField(required=False, allow_null=True)
162
+ compression = serializers.BooleanField(required=False, allow_null=True)
163
+
164
+
165
+ class AxesConfigSerializer(serializers.Serializer):
166
+ """Django-Axes brute-force protection configuration."""
167
+ enabled = serializers.BooleanField(required=False, allow_null=True)
168
+ failure_limit = serializers.IntegerField(required=False, allow_null=True)
169
+ cooloff_time = serializers.IntegerField(required=False, allow_null=True)
170
+ lock_out_at_failure = serializers.BooleanField(required=False, allow_null=True)
171
+ reset_on_success = serializers.BooleanField(required=False, allow_null=True)
172
+ only_user_failures = serializers.BooleanField(required=False, allow_null=True)
173
+ lockout_template = serializers.CharField(required=False, allow_null=True)
174
+ lockout_url = serializers.CharField(required=False, allow_null=True)
175
+ verbose = serializers.BooleanField(required=False, allow_null=True)
176
+ enable_access_failure_log = serializers.BooleanField(required=False, allow_null=True)
177
+ ipware_proxy_count = serializers.IntegerField(required=False, allow_null=True)
178
+ ipware_meta_precedence_order = serializers.ListField(required=False, allow_null=True)
179
+ allowed_ips = serializers.ListField(required=False, allow_null=True)
180
+ denied_ips = serializers.ListField(required=False, allow_null=True)
181
+ cache_name = serializers.CharField(required=False, allow_null=True)
182
+ use_user_agent = serializers.BooleanField(required=False, allow_null=True)
183
+ username_form_field = serializers.CharField(required=False, allow_null=True)
184
+
185
+
186
+ class NextJSAdminConfigSerializer(serializers.Serializer):
187
+ """Next.js Admin application configuration."""
188
+ enabled = serializers.BooleanField(required=False, allow_null=True)
189
+ url = serializers.CharField(required=False, allow_null=True)
190
+ api_base_url = serializers.CharField(required=False, allow_null=True)
191
+
192
+
193
+ class ConstanceConfigSerializer(serializers.Serializer):
194
+ """Django-Constance dynamic settings configuration."""
195
+ config = serializers.DictField(required=False, allow_null=True)
196
+ config_fieldsets = serializers.DictField(required=False, allow_null=True)
197
+ backend = serializers.CharField(required=False, allow_null=True)
198
+ database_prefix = serializers.CharField(required=False, allow_null=True)
199
+ database_cache_backend = serializers.CharField(required=False, allow_null=True)
200
+ additional_config = serializers.DictField(required=False, allow_null=True)
201
+
202
+
203
+ class OpenAPIClientConfigSerializer(serializers.Serializer):
204
+ """OpenAPI Client generation configuration."""
205
+ enabled = serializers.BooleanField(required=False, allow_null=True)
206
+ output_dir = serializers.CharField(required=False, allow_null=True)
207
+ client_name = serializers.CharField(required=False, allow_null=True)
208
+ schema_url = serializers.CharField(required=False, allow_null=True)
209
+ generator = serializers.CharField(required=False, allow_null=True)
210
+ additional_properties = serializers.DictField(required=False, allow_null=True)
211
+ templates = serializers.ListField(required=False, allow_null=True)
212
+ global_properties = serializers.DictField(required=False, allow_null=True)
213
+
214
+
129
215
  class DjangoConfigSerializer(serializers.Serializer):
130
216
  """
131
217
  Typed serializer for user's DjangoConfig settings.
@@ -188,10 +274,10 @@ class DjangoConfigSerializer(serializers.Serializer):
188
274
  spectacular = SpectacularConfigSerializer(required=False, allow_null=True)
189
275
  jwt = JWTConfigSerializer(required=False, allow_null=True)
190
276
 
191
- # Other configs (pass-through for flexibility)
192
- telegram = serializers.DictField(required=False, allow_null=True)
193
- ngrok = serializers.DictField(required=False, allow_null=True)
194
- axes = serializers.DictField(required=False, allow_null=True)
277
+ # Services & Security (now typed!)
278
+ telegram = TelegramConfigSerializer(required=False, allow_null=True)
279
+ ngrok = NgrokConfigSerializer(required=False, allow_null=True)
280
+ axes = AxesConfigSerializer(required=False, allow_null=True)
195
281
  crypto_fields = serializers.DictField(required=False, allow_null=True)
196
282
  unfold = serializers.CharField(required=False, allow_null=True) # String representation of Unfold config
197
283
  tailwind_app_name = serializers.CharField(required=False, allow_null=True)
@@ -199,10 +285,10 @@ class DjangoConfigSerializer(serializers.Serializer):
199
285
  limits = serializers.DictField(required=False, allow_null=True)
200
286
  api_keys = serializers.DictField(required=False, allow_null=True)
201
287
  custom_middleware = serializers.ListField(required=False, allow_null=True)
202
- nextjs_admin = serializers.DictField(required=False, allow_null=True)
288
+ nextjs_admin = NextJSAdminConfigSerializer(required=False, allow_null=True)
203
289
  admin_emails = serializers.ListField(required=False, allow_null=True)
204
- constance = serializers.DictField(required=False, allow_null=True)
205
- openapi_client = serializers.DictField(required=False, allow_null=True)
290
+ constance = ConstanceConfigSerializer(required=False, allow_null=True)
291
+ openapi_client = OpenAPIClientConfigSerializer(required=False, allow_null=True)
206
292
 
207
293
  # Metadata
208
294
  _meta = ConfigMetaSerializer(required=False, allow_null=True)
@@ -238,7 +324,7 @@ class ConfigDataSerializer(serializers.Serializer):
238
324
  help_text="User's DjangoConfig settings"
239
325
  )
240
326
  django_settings = serializers.DictField(
241
- help_text="Complete Django settings (sanitized)"
327
+ help_text="Complete Django settings (sanitized, contains mixed types)"
242
328
  )
243
329
  _validation = ConfigValidationSerializer(
244
330
  help_text="Validation result comparing serializer with actual config"
@@ -36,11 +36,16 @@ class UserStatisticsSerializer(serializers.Serializer):
36
36
  superusers = serializers.IntegerField(help_text="Number of superusers")
37
37
 
38
38
 
39
+ class AppStatisticsDataSerializer(serializers.Serializer):
40
+ """Serializer for application statistics data."""
41
+
42
+ name = serializers.CharField(help_text="Human-readable app name")
43
+ total_records = serializers.IntegerField(help_text="Total records count")
44
+ model_count = serializers.IntegerField(help_text="Number of models")
45
+
46
+
39
47
  class AppStatisticsSerializer(serializers.Serializer):
40
48
  """Serializer for application-specific statistics."""
41
49
 
42
50
  app_name = serializers.CharField(help_text="Application name")
43
- statistics = serializers.DictField(
44
- child=serializers.IntegerField(),
45
- help_text="Application statistics"
46
- )
51
+ statistics = AppStatisticsDataSerializer(help_text="Application statistics")
@@ -55,7 +55,7 @@ class CommandsService:
55
55
  continue
56
56
 
57
57
  try:
58
- # Try to load command to get help text
58
+ # Try to load command to get help text and metadata
59
59
  command = load_command_class(app_name, command_name)
60
60
  help_text = getattr(command, 'help', 'No description available')
61
61
 
@@ -66,6 +66,11 @@ class CommandsService:
66
66
  # Get risk level
67
67
  risk_level = get_command_risk_level(command_name, app_name)
68
68
 
69
+ # Extract security metadata from command instance
70
+ web_executable = getattr(command, 'web_executable', None)
71
+ requires_input = getattr(command, 'requires_input', None)
72
+ is_destructive = getattr(command, 'is_destructive', None)
73
+
69
74
  commands_list.append({
70
75
  'name': command_name,
71
76
  'app': app_name,
@@ -74,6 +79,9 @@ class CommandsService:
74
79
  'is_custom': is_custom,
75
80
  'is_allowed': is_allowed,
76
81
  'risk_level': risk_level,
82
+ 'web_executable': web_executable,
83
+ 'requires_input': requires_input,
84
+ 'is_destructive': is_destructive,
77
85
  })
78
86
  except Exception as e:
79
87
  # If we can't load the command, still include basic info
@@ -86,6 +94,9 @@ class CommandsService:
86
94
  'is_custom': app_name == 'django_cfg',
87
95
  'is_allowed': is_allowed,
88
96
  'risk_level': get_command_risk_level(command_name, app_name),
97
+ 'web_executable': None,
98
+ 'requires_input': None,
99
+ 'is_destructive': None,
89
100
  })
90
101
 
91
102
  # Sort by name
@@ -130,17 +130,23 @@ class ZipExtractionMixin:
130
130
  @method_decorator(xframe_options_exempt, name='dispatch')
131
131
  class NextJSStaticView(ZipExtractionMixin, View):
132
132
  """
133
- Serve Next.js static build files with automatic JWT token injection.
133
+ Serve Next.js static build files with automatic JWT token injection and precompression support.
134
134
 
135
135
  Features:
136
136
  - Serves Next.js static export files like a static file server
137
137
  - Smart ZIP extraction: compares ZIP metadata (size + mtime) with marker file
138
- - Automatically injects JWT tokens for authenticated users
139
- - Tokens injected into HTML responses only
138
+ - Automatically injects JWT tokens for authenticated users (HTML only)
139
+ - **Precompression support**: Automatically serves .br or .gz files if available
140
140
  - Handles Next.js client-side routing (.html fallback)
141
141
  - Automatically serves index.html for directory paths
142
142
  - X-Frame-Options exempt to allow embedding in iframes
143
143
 
144
+ Compression Strategy:
145
+ - Brotli (.br) preferred over Gzip (.gz) - ~5-15% better compression
146
+ - Automatically detects browser support via Accept-Encoding header
147
+ - Skips compression for HTML files (JWT injection requires uncompressed content)
148
+ - Only serves precompressed files, no runtime compression
149
+
144
150
  ZIP Extraction Logic:
145
151
  - If directory doesn't exist: extract from ZIP
146
152
  - If marker file missing: extract from ZIP
@@ -154,12 +160,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
154
160
  - /cfg/admin/private/ → /cfg/admin/private.html (fallback)
155
161
  - /cfg/admin/tasks → /cfg/admin/tasks.html
156
162
  - /cfg/admin/tasks → /cfg/admin/tasks/index.html (fallback)
163
+
164
+ Compression examples:
165
+ - _app.js (br supported) → _app.js.br + Content-Encoding: br
166
+ - _app.js (gzip supported) → _app.js.gz + Content-Encoding: gzip
167
+ - _app.js (no support) → _app.js (uncompressed)
168
+ - index.html → index.html (never compressed, needs JWT injection)
157
169
  """
158
170
 
159
171
  app_name = 'admin'
160
172
 
161
173
  def get(self, request, path=''):
162
- """Serve static files from Next.js build with JWT injection."""
174
+ """Serve static files from Next.js build with JWT injection and compression support."""
163
175
  import django_cfg
164
176
 
165
177
  base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / self.app_name
@@ -191,8 +203,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
191
203
  request.META.pop('HTTP_IF_MODIFIED_SINCE', None)
192
204
  request.META.pop('HTTP_IF_NONE_MATCH', None)
193
205
 
194
- # Serve the static file
195
- response = serve(request, path, document_root=str(base_dir))
206
+ # Try to serve precompressed file if browser supports it
207
+ compressed_path, encoding = self._find_precompressed_file(base_dir, path, request)
208
+ if compressed_path:
209
+ logger.debug(f"[Compression] Serving {encoding} for {path}")
210
+ response = serve(request, compressed_path, document_root=str(base_dir))
211
+ response['Content-Encoding'] = encoding
212
+ # Remove Content-Length as it's incorrect for compressed content
213
+ if 'Content-Length' in response:
214
+ del response['Content-Length']
215
+ else:
216
+ # Serve the static file normally
217
+ response = serve(request, path, document_root=str(base_dir))
196
218
 
197
219
  # Convert FileResponse to HttpResponse for HTML files to enable JWT injection
198
220
  if isinstance(response, FileResponse):
@@ -222,6 +244,65 @@ class NextJSStaticView(ZipExtractionMixin, View):
222
244
 
223
245
  return response
224
246
 
247
+ def _find_precompressed_file(self, base_dir, path, request):
248
+ """
249
+ Find and return precompressed file (.br or .gz) if available and supported by browser.
250
+
251
+ Brotli (.br) is preferred over Gzip (.gz) as it provides better compression.
252
+
253
+ Args:
254
+ base_dir: Base directory for static files
255
+ path: Requested file path
256
+ request: Django request object
257
+
258
+ Returns:
259
+ tuple: (compressed_path, encoding) if precompressed file found and supported,
260
+ (None, None) otherwise
261
+
262
+ Examples:
263
+ _app.js → _app.js.br (if Accept-Encoding: br)
264
+ _app.js → _app.js.gz (if Accept-Encoding: gzip, no .br)
265
+ _app.js → (None, None) (if no precompressed files or not supported)
266
+ """
267
+ # Get Accept-Encoding header
268
+ accept_encoding = request.META.get('HTTP_ACCEPT_ENCODING', '').lower()
269
+
270
+ # Check if browser supports brotli (preferred) or gzip
271
+ supports_br = 'br' in accept_encoding
272
+ supports_gzip = 'gzip' in accept_encoding
273
+
274
+ if not (supports_br or supports_gzip):
275
+ return None, None
276
+
277
+ # Don't compress HTML files - we need to inject JWT tokens
278
+ # JWT injection requires modifying content, which is incompatible with compression
279
+ if path.endswith('.html'):
280
+ return None, None
281
+
282
+ # Build full file path
283
+ file_path = base_dir / path
284
+
285
+ # Check if original file exists (safety check)
286
+ if not file_path.exists() or not file_path.is_file():
287
+ return None, None
288
+
289
+ # Try Brotli first (better compression, ~5-15% smaller than gzip)
290
+ if supports_br:
291
+ br_path = f"{path}.br"
292
+ br_file = base_dir / br_path
293
+ if br_file.exists() and br_file.is_file():
294
+ return br_path, 'br'
295
+
296
+ # Fallback to Gzip
297
+ if supports_gzip:
298
+ gz_path = f"{path}.gz"
299
+ gz_file = base_dir / gz_path
300
+ if gz_file.exists() and gz_file.is_file():
301
+ return gz_path, 'gzip'
302
+
303
+ # No precompressed file found or not supported
304
+ return None, None
305
+
225
306
  def _resolve_spa_path(self, base_dir, path):
226
307
  """
227
308
  Resolve SPA path with multiple fallback strategies.
@@ -7,16 +7,19 @@ Usage: python manage.py maintenance enable/disable/status/sync domain.com
7
7
 
8
8
  from typing import Any
9
9
 
10
- from django.core.management.base import BaseCommand, CommandError
10
+ from django.core.management.base import CommandError
11
11
  from django.db import transaction
12
12
 
13
+ from django_cfg.management.utils import InteractiveCommand
14
+
13
15
  from ...models import CloudflareSite, MaintenanceLog
14
16
  from ...services import MaintenanceService
15
17
 
16
18
 
17
- class Command(BaseCommand):
19
+ class Command(InteractiveCommand):
18
20
  """Simple maintenance management command."""
19
21
 
22
+ command_name = 'maintenance'
20
23
  help = 'Manage maintenance mode for Cloudflare sites'
21
24
 
22
25
  def add_arguments(self, parser) -> None:
@@ -5,16 +5,18 @@ Handles automatic start/stop of scheduled maintenance windows.
5
5
  """
6
6
 
7
7
 
8
- from django.core.management.base import BaseCommand
9
8
  from django.utils import timezone
10
9
 
10
+ from django_cfg.management.utils import AdminCommand
11
+
11
12
  from ...models import ScheduledMaintenance
12
13
  from ...services.scheduled_maintenance_service import scheduled_maintenance_service
13
14
 
14
15
 
15
- class Command(BaseCommand):
16
+ class Command(AdminCommand):
16
17
  """Process scheduled maintenance events."""
17
18
 
19
+ command_name = 'process_scheduled_maintenance'
18
20
  help = 'Process scheduled maintenance events (start due, complete overdue)'
19
21
 
20
22
  def add_arguments(self, parser):