super_admin 0.2.0

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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +216 -0
  3. data/Rakefile +30 -0
  4. data/app/assets/stylesheets/super_admin/application.css +15 -0
  5. data/app/assets/stylesheets/super_admin/tailwind.css +1 -0
  6. data/app/assets/stylesheets/super_admin/tailwind.source.css +25 -0
  7. data/app/controllers/super_admin/application_controller.rb +89 -0
  8. data/app/controllers/super_admin/associations_controller.rb +136 -0
  9. data/app/controllers/super_admin/audit_logs_controller.rb +39 -0
  10. data/app/controllers/super_admin/base_controller.rb +133 -0
  11. data/app/controllers/super_admin/dashboard_controller.rb +29 -0
  12. data/app/controllers/super_admin/exports_controller.rb +109 -0
  13. data/app/controllers/super_admin/resources_controller.rb +201 -0
  14. data/app/dashboards/super_admin/base_dashboard.rb +200 -0
  15. data/app/errors/super_admin/configuration_error.rb +6 -0
  16. data/app/helpers/super_admin/application_helper.rb +84 -0
  17. data/app/helpers/super_admin/exports_helper.rb +16 -0
  18. data/app/helpers/super_admin/resources_helper.rb +204 -0
  19. data/app/helpers/super_admin/route_helper.rb +7 -0
  20. data/app/javascript/super_admin/application.js +263 -0
  21. data/app/jobs/super_admin/application_job.rb +4 -0
  22. data/app/jobs/super_admin/generate_super_admin_csv_export_job.rb +100 -0
  23. data/app/mailers/super_admin/application_mailer.rb +6 -0
  24. data/app/models/super_admin/application_record.rb +5 -0
  25. data/app/models/super_admin/audit_log.rb +35 -0
  26. data/app/models/super_admin/csv_export.rb +67 -0
  27. data/app/services/super_admin/auditing.rb +74 -0
  28. data/app/services/super_admin/authorization.rb +113 -0
  29. data/app/services/super_admin/authorization_adapters/base_adapter.rb +100 -0
  30. data/app/services/super_admin/authorization_adapters/default_adapter.rb +77 -0
  31. data/app/services/super_admin/authorization_adapters/proc_adapter.rb +65 -0
  32. data/app/services/super_admin/authorization_adapters/pundit_adapter.rb +81 -0
  33. data/app/services/super_admin/csv_export_creator.rb +45 -0
  34. data/app/services/super_admin/dashboard_registry.rb +90 -0
  35. data/app/services/super_admin/dashboard_resolver.rb +100 -0
  36. data/app/services/super_admin/filter_builder.rb +185 -0
  37. data/app/services/super_admin/form_builder.rb +59 -0
  38. data/app/services/super_admin/form_fields/array_field.rb +35 -0
  39. data/app/services/super_admin/form_fields/association_field.rb +146 -0
  40. data/app/services/super_admin/form_fields/base_field.rb +53 -0
  41. data/app/services/super_admin/form_fields/boolean_field.rb +29 -0
  42. data/app/services/super_admin/form_fields/date_field.rb +15 -0
  43. data/app/services/super_admin/form_fields/date_time_field.rb +15 -0
  44. data/app/services/super_admin/form_fields/enum_field.rb +27 -0
  45. data/app/services/super_admin/form_fields/factory.rb +102 -0
  46. data/app/services/super_admin/form_fields/nested_field.rb +120 -0
  47. data/app/services/super_admin/form_fields/number_field.rb +29 -0
  48. data/app/services/super_admin/form_fields/text_area_field.rb +19 -0
  49. data/app/services/super_admin/model_inspector.rb +182 -0
  50. data/app/services/super_admin/queries/base_query.rb +45 -0
  51. data/app/services/super_admin/queries/filter_query.rb +188 -0
  52. data/app/services/super_admin/queries/resource_scope_query.rb +74 -0
  53. data/app/services/super_admin/queries/search_query.rb +146 -0
  54. data/app/services/super_admin/queries/sort_query.rb +41 -0
  55. data/app/services/super_admin/resource_configuration.rb +63 -0
  56. data/app/services/super_admin/resource_exporter.rb +78 -0
  57. data/app/services/super_admin/resource_query.rb +40 -0
  58. data/app/services/super_admin/resources/association_inspector.rb +112 -0
  59. data/app/services/super_admin/resources/collection_presenter.rb +63 -0
  60. data/app/services/super_admin/resources/context.rb +63 -0
  61. data/app/services/super_admin/resources/filter_params.rb +29 -0
  62. data/app/services/super_admin/resources/permitted_attributes.rb +104 -0
  63. data/app/services/super_admin/resources/value_normalizer.rb +121 -0
  64. data/app/services/super_admin/sensitive_attributes.rb +166 -0
  65. data/app/views/layouts/super_admin.html.erb +74 -0
  66. data/app/views/super_admin/audit_logs/index.html.erb +143 -0
  67. data/app/views/super_admin/dashboard/index.html.erb +79 -0
  68. data/app/views/super_admin/exports/index.html.erb +84 -0
  69. data/app/views/super_admin/exports/show.html.erb +57 -0
  70. data/app/views/super_admin/resources/_form.html.erb +42 -0
  71. data/app/views/super_admin/resources/destroy.turbo_stream.erb +17 -0
  72. data/app/views/super_admin/resources/edit.html.erb +37 -0
  73. data/app/views/super_admin/resources/index.html.erb +189 -0
  74. data/app/views/super_admin/resources/new.html.erb +31 -0
  75. data/app/views/super_admin/resources/show.html.erb +106 -0
  76. data/app/views/super_admin/shared/_breadcrumbs.html.erb +12 -0
  77. data/app/views/super_admin/shared/_custom_styles.html.erb +132 -0
  78. data/app/views/super_admin/shared/_flash.html.erb +55 -0
  79. data/app/views/super_admin/shared/_form_field.html.erb +35 -0
  80. data/app/views/super_admin/shared/_navigation.html.erb +92 -0
  81. data/app/views/super_admin/shared/_nested_fields.html.erb +59 -0
  82. data/app/views/super_admin/shared/_nested_record_fields.html.erb +45 -0
  83. data/config/importmap.rb +4 -0
  84. data/config/initializers/rack_attack.rb +134 -0
  85. data/config/initializers/super_admin.rb +117 -0
  86. data/config/locales/super_admin.en.yml +197 -0
  87. data/config/locales/super_admin.fr.yml +197 -0
  88. data/config/routes.rb +22 -0
  89. data/lib/generators/super_admin/dashboard_generator.rb +50 -0
  90. data/lib/generators/super_admin/install_generator.rb +58 -0
  91. data/lib/generators/super_admin/templates/20240101000001_create_super_admin_audit_logs.rb +24 -0
  92. data/lib/generators/super_admin/templates/20240101000002_create_super_admin_csv_exports.rb +33 -0
  93. data/lib/generators/super_admin/templates/super_admin.rb +58 -0
  94. data/lib/super_admin/dashboard_creator.rb +256 -0
  95. data/lib/super_admin/engine.rb +53 -0
  96. data/lib/super_admin/install_task.rb +96 -0
  97. data/lib/super_admin/version.rb +3 -0
  98. data/lib/super_admin.rb +7 -0
  99. data/lib/tasks/super_admin_tasks.rake +38 -0
  100. metadata +239 -0
@@ -0,0 +1,92 @@
1
+ <aside id="super-admin-sidebar" data-super-admin--mobile-menu-target="sidebar" class="fixed lg:static inset-y-0 left-0 z-40 w-64 bg-gray-900 text-white flex-shrink-0 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out">
2
+ <div class="flex flex-col h-full">
3
+ <!-- Logo -->
4
+ <div class="px-6 py-4 border-b border-gray-800 flex items-center justify-between">
5
+ <h1 class="text-xl lg:text-2xl font-bold text-white truncate"><%= t("super_admin.navigation.brand") %></h1>
6
+ <!-- Close button for mobile -->
7
+ <button data-action="click->super-admin--mobile-menu#toggle" class="lg:hidden inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-800 focus:outline-none">
8
+ <span class="sr-only">Close menu</span>
9
+ <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
10
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
11
+ </svg>
12
+ </button>
13
+ </div>
14
+
15
+ <!-- Navigation -->
16
+ <nav class="flex-1 px-3 lg:px-4 py-4 lg:py-6 overflow-y-auto">
17
+ <!-- Return to App -->
18
+ <div class="mb-4 lg:mb-6">
19
+ <% return_path = if defined?(main_app) && main_app.respond_to?(:root_path)
20
+ main_app.root_path
21
+ else
22
+ super_admin_root_path
23
+ end %>
24
+ <%= link_to return_path, class: "flex items-center px-3 lg:px-4 py-2 text-sm font-medium rounded-md text-gray-300 hover-bg-gray-800 hover-text-white transition-colors" do %>
25
+ <svg class="mr-2 lg:mr-3 h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
27
+ </svg>
28
+ <span class="truncate"><%= t("super_admin.navigation.return_to_app") %></span>
29
+ <% end %>
30
+ </div>
31
+
32
+ <!-- Dashboard -->
33
+ <div class="mb-2">
34
+ <%= link_to super_admin_root_path, class: "flex items-center px-3 lg:px-4 py-2 text-sm font-medium rounded-md hover-bg-gray-800 #{current_page?(super_admin_root_path) ? 'bg-gray-800 text-white' : 'text-gray-300'}" do %>
35
+ <svg class="mr-2 lg:mr-3 h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
37
+ </svg>
38
+ <span class="truncate"><%= t("super_admin.navigation.dashboard") %></span>
39
+ <% end %>
40
+ </div>
41
+
42
+ <!-- Exports -->
43
+ <div class="mb-2">
44
+ <%= link_to super_admin_exports_path, class: "flex items-center px-3 lg:px-4 py-2 text-sm font-medium rounded-md hover-bg-gray-800 #{current_page?(super_admin_exports_path) ? 'bg-gray-800 text-white' : 'text-gray-300'}" do %>
45
+ <svg class="mr-2 lg:mr-3 h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
46
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
47
+ </svg>
48
+ <span class="truncate"><%= t("super_admin.navigation.exports") %></span>
49
+ <% end %>
50
+ </div>
51
+
52
+ <!-- Audit Logs -->
53
+ <div class="mb-4 lg:mb-6">
54
+ <% audit_active = controller_name == "audit_logs" %>
55
+ <%= link_to super_admin_audit_logs_path, class: "flex items-center px-3 lg:px-4 py-2 text-sm font-medium rounded-md hover-bg-gray-800 #{audit_active ? 'bg-gray-800 text-white' : 'text-gray-300'}" do %>
56
+ <svg class="mr-2 lg:mr-3 h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V5a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path>
58
+ </svg>
59
+ <span class="truncate"><%= t("super_admin.navigation.audit_logs") %></span>
60
+ <% end %>
61
+ </div>
62
+
63
+ <!-- Models List -->
64
+ <div>
65
+ <h3 class="px-3 lg:px-4 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">
66
+ <%= t("super_admin.navigation.models_heading", count: available_models.count) %>
67
+ </h3>
68
+ <div class="space-y-1">
69
+ <% available_models.each do |model_class| %>
70
+ <% resource_name = model_class.name.underscore.pluralize %>
71
+ <% is_active = params[:resource] == resource_name %>
72
+ <%
73
+ begin
74
+ record_count = model_class.count
75
+ rescue ActiveRecord::StatementInvalid, StandardError
76
+ record_count = "—"
77
+ end
78
+ %>
79
+ <%= link_to model_path(model_class), class: "flex items-center justify-between px-3 lg:px-4 py-2 text-sm rounded-md hover-bg-gray-800 #{is_active ? 'bg-gray-800 text-white' : 'text-gray-300'}" do %>
80
+ <span class="truncate flex-1 min-w-0">
81
+ <%= model_display_name(model_class) %>
82
+ </span>
83
+ <span class="ml-2 text-xs font-semibold flex-shrink-0 <%= is_active ? 'text-blue-400 bg-blue-900-opacity' : 'text-gray-400 bg-gray-800' %> px-2 py-0.5 rounded-full">
84
+ <%= record_count %>
85
+ </span>
86
+ <% end %>
87
+ <% end %>
88
+ </div>
89
+ </div>
90
+ </nav>
91
+ </div>
92
+ </aside>
@@ -0,0 +1,59 @@
1
+ <%# Rendu des champs nested attributes pour une association accepts_nested_attributes_for %>
2
+ <%# Locals: form, parent_model_class, association, nested_model_class, nested_attributes, nested_options, label, current_depth %>
3
+
4
+ <% association_name = association.name %>
5
+ <% current_depth ||= 0 %>
6
+ <% max_depth = SuperAdmin.max_nested_depth %>
7
+
8
+ <% new_record_template = capture do %>
9
+ <%= form.fields_for(association_name, association.klass.new, child_index: "__NEW_RECORD__") do |nested_form| %>
10
+ <%= render "super_admin/shared/nested_record_fields",
11
+ form: nested_form,
12
+ association: association,
13
+ nested_model_class: nested_model_class,
14
+ nested_attributes: nested_attributes,
15
+ nested_options: nested_options,
16
+ new_record: true,
17
+ current_depth: current_depth + 1 %>
18
+ <% end %>
19
+ <% end %>
20
+
21
+ <div class="mb-6 space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4"
22
+ data-controller="super-admin--nested-form">
23
+ <div class="flex items-center justify-between">
24
+ <h3 class="text-base font-semibold text-gray-800">
25
+ <%= label %>
26
+ <% if current_depth > 0 %>
27
+ <span class="ml-2 text-xs text-gray-500">(niveau <%= current_depth + 1 %>/<%= max_depth %>)</span>
28
+ <% end %>
29
+ </h3>
30
+ <% if association.collection? %>
31
+ <span class="text-xs text-gray-500"><%= t("super_admin.resources.nested.collection_hint") %></span>
32
+ <% end %>
33
+ </div>
34
+
35
+ <div class="space-y-4" data-super-admin--nested-form-target="entries">
36
+ <%= form.fields_for association_name do |nested_form| %>
37
+ <%= render "super_admin/shared/nested_record_fields",
38
+ form: nested_form,
39
+ association: association,
40
+ nested_model_class: nested_model_class,
41
+ nested_attributes: nested_attributes,
42
+ nested_options: nested_options,
43
+ new_record: nested_form.object.new_record?,
44
+ current_depth: current_depth + 1 %>
45
+ <% end %>
46
+ </div>
47
+
48
+ <template data-super-admin--nested-form-target="template">
49
+ <%= new_record_template %>
50
+ </template>
51
+
52
+ <div class="pt-2">
53
+ <button type="button"
54
+ class="inline-flex items-center rounded-md border border-dashed border-blue-300 px-3 py-2 text-sm font-medium text-blue-600 hover-border-blue-400 hover-text-blue-700"
55
+ data-action="super-admin--nested-form#add">
56
+ <%= t("super_admin.resources.nested.add", model: nested_model_class.model_name.human(count: 1)) %>
57
+ </button>
58
+ </div>
59
+ </div>
@@ -0,0 +1,45 @@
1
+ <%# Rendu d'un enregistrement nested %>
2
+ <%# Locals: form, association, nested_model_class, nested_attributes, nested_options, new_record, current_depth %>
3
+
4
+ <% current_depth ||= 1 %>
5
+ <% removable = nested_options[:allow_destroy] || new_record %>
6
+ <% entry_title = if form.object.respond_to?(:persisted?) && form.object.persisted?
7
+ t("super_admin.resources.nested.entry_existing", model: nested_model_class.model_name.human(count: 1), id: form.object.id)
8
+ else
9
+ t("super_admin.resources.nested.entry_new", model: nested_model_class.model_name.human(count: 1))
10
+ end %>
11
+
12
+ <div class="space-y-4 rounded-md border border-gray-200 bg-white p-4"
13
+ data-super-admin--nested-form-entry>
14
+ <div class="flex items-center justify-between">
15
+ <p class="text-sm font-medium text-gray-800">
16
+ <%= entry_title %>
17
+ </p>
18
+ <% if removable %>
19
+ <button type="button"
20
+ class="text-sm font-medium text-red-600 hover:text-red-700"
21
+ data-action="super-admin--nested-form#remove"
22
+ data-super-admin--nested-form-remove>
23
+ <%= t("super_admin.resources.nested.remove") %>
24
+ </button>
25
+ <% end %>
26
+ </div>
27
+
28
+ <% if form.object.respond_to?(:persisted?) && form.object.persisted? %>
29
+ <%= form.hidden_field :id %>
30
+ <% end %>
31
+
32
+ <% if nested_options[:allow_destroy] %>
33
+ <%= form.hidden_field :_destroy, value: "0" %>
34
+ <% end %>
35
+
36
+ <% nested_attributes.each do |attribute| %>
37
+ <%= render "super_admin/shared/form_field", form: form, model_class: nested_model_class, attribute_name: attribute %>
38
+ <% end %>
39
+
40
+ <% if nested_options[:allow_destroy] && form.object.respond_to?(:persisted?) && form.object.persisted? %>
41
+ <p class="text-xs text-gray-500">
42
+ <%= t("super_admin.resources.nested.remove_hint") %>
43
+ </p>
44
+ <% end %>
45
+ </div>
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SuperAdmin JavaScript - single bundled file
4
+ pin "super_admin/application"
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure Rack::Attack for SuperAdmin endpoints
4
+ # This protects against common attacks like brute force, DoS, and abuse
5
+
6
+ module SuperAdmin
7
+ class RackAttackConfiguration
8
+ class << self
9
+ def configure
10
+ return unless defined?(Rack::Attack)
11
+
12
+ configure_throttles
13
+ configure_blocklists
14
+ configure_safelists
15
+ configure_tracking
16
+ end
17
+
18
+ private
19
+
20
+ def configure_throttles
21
+ # Throttle searches to prevent DoS
22
+ Rack::Attack.throttle("super_admin/searches/ip", limit: 30, period: 60) do |req|
23
+ if req.path.start_with?("/super_admin") && (req.get? || req.post?) && req.path.include?("search")
24
+ req.ip
25
+ end
26
+ end
27
+
28
+ # Throttle association search API (more generous as it's used heavily in forms)
29
+ Rack::Attack.throttle("super_admin/api/associations/ip", limit: 100, period: 60) do |req|
30
+ if req.path == "/super_admin/associations/search" && req.get?
31
+ req.ip
32
+ end
33
+ end
34
+
35
+ # Throttle CSV exports to prevent abuse
36
+ Rack::Attack.throttle("super_admin/exports/ip", limit: 5, period: 300) do |req|
37
+ if req.path.start_with?("/super_admin") && req.post? && req.path.include?("export")
38
+ req.ip
39
+ end
40
+ end
41
+
42
+ # Throttle bulk operations (more restrictive)
43
+ Rack::Attack.throttle("super_admin/bulk/ip", limit: 10, period: 60) do |req|
44
+ if req.path.match?(%r{/super_admin/.+/bulk}) && req.post?
45
+ req.ip
46
+ end
47
+ end
48
+
49
+ # Throttle write operations (create/update/delete)
50
+ Rack::Attack.throttle("super_admin/writes/ip", limit: 60, period: 60) do |req|
51
+ if req.path.start_with?("/super_admin") && (req.post? || req.patch? || req.put? || req.delete?)
52
+ req.ip
53
+ end
54
+ end
55
+
56
+ # Global throttle for all SuperAdmin requests
57
+ Rack::Attack.throttle("super_admin/global/ip", limit: 300, period: 60) do |req|
58
+ if req.path.start_with?("/super_admin")
59
+ req.ip
60
+ end
61
+ end
62
+ end
63
+
64
+ def configure_blocklists
65
+ # Block requests from known bad actors (can be configured via environment)
66
+ Rack::Attack.blocklist("super_admin/blocked_ips") do |req|
67
+ if req.path.start_with?("/super_admin")
68
+ blocked_ips = ENV.fetch("SUPER_ADMIN_BLOCKED_IPS", "").split(",").map(&:strip)
69
+ blocked_ips.include?(req.ip)
70
+ end
71
+ end
72
+
73
+ # Block requests with suspicious patterns in query params
74
+ Rack::Attack.blocklist("super_admin/sql_injection_attempts") do |req|
75
+ if req.path.start_with?("/super_admin")
76
+ query_string = req.query_string.to_s.downcase
77
+ # Detect common SQL injection patterns
78
+ query_string.match?(/(\bunion\b|\bselect\b|\binsert\b|\bupdate\b|\bdelete\b|\bdrop\b).*(\bfrom\b|\binto\b|\btable\b)/)
79
+ end
80
+ end
81
+ end
82
+
83
+ def configure_safelists
84
+ # Safelist requests from localhost in development
85
+ Rack::Attack.safelist("super_admin/localhost") do |req|
86
+ if req.path.start_with?("/super_admin")
87
+ Rails.env.development? && [ "127.0.0.1", "::1" ].include?(req.ip)
88
+ end
89
+ end
90
+
91
+ # Allow configurable safelist via environment
92
+ Rack::Attack.safelist("super_admin/safelisted_ips") do |req|
93
+ if req.path.start_with?("/super_admin")
94
+ safe_ips = ENV.fetch("SUPER_ADMIN_SAFE_IPS", "").split(",").map(&:strip)
95
+ safe_ips.include?(req.ip)
96
+ end
97
+ end
98
+ end
99
+
100
+ def configure_tracking
101
+ # Track requests for monitoring (optional, requires Rails cache)
102
+ Rack::Attack.track("super_admin/requests") do |req|
103
+ req.path.start_with?("/super_admin")
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ # Auto-configure if Rack::Attack is available
111
+ if defined?(Rack::Attack)
112
+ SuperAdmin::RackAttackConfiguration.configure
113
+
114
+ # Custom response for throttled requests
115
+ Rack::Attack.throttled_responder = lambda do |req|
116
+ match_data = req.env["rack.attack.match_data"]
117
+ now = match_data[:epoch_time]
118
+
119
+ headers = {
120
+ "Content-Type" => "application/json",
121
+ "X-RateLimit-Limit" => match_data[:limit].to_s,
122
+ "X-RateLimit-Remaining" => "0",
123
+ "X-RateLimit-Reset" => (now + (match_data[:period] - now % match_data[:period])).to_s
124
+ }
125
+
126
+ body = {
127
+ error: "Rate limit exceeded",
128
+ message: "Too many requests. Please try again later.",
129
+ retry_after: match_data[:period] - now % match_data[:period]
130
+ }
131
+
132
+ [ 429, headers, [ body.to_json ] ]
133
+ end
134
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/delegation"
4
+
5
+ module SuperAdmin
6
+ class Configuration
7
+ attr_accessor :max_nested_depth,
8
+ :association_select_limit,
9
+ :association_pagination_limit,
10
+ :enable_association_search,
11
+ :authorization_adapter,
12
+ :on_unauthorized,
13
+ :current_user_method,
14
+ :layout,
15
+ :default_locale,
16
+ :parent_controller,
17
+ :user_class,
18
+ :super_admin_check
19
+
20
+ attr_reader :authorize_with, :authenticate_with
21
+
22
+ attr_reader :additional_sensitive_attributes
23
+
24
+ def initialize
25
+ @max_nested_depth = 2
26
+ @association_select_limit = 10
27
+ @association_pagination_limit = 20
28
+ @enable_association_search = true
29
+
30
+ @authorize_with = nil
31
+ @authorization_adapter = :auto
32
+ @on_unauthorized = nil
33
+
34
+ @authenticate_with = nil
35
+ @current_user_method = :current_user
36
+ @user_class = "User"
37
+
38
+ @layout = "super_admin"
39
+ @default_locale = :fr
40
+ @parent_controller = "::ApplicationController"
41
+
42
+ @super_admin_check = nil
43
+ @additional_sensitive_attributes = []
44
+ end
45
+
46
+ def authorize_with(value = nil, &block)
47
+ if block_given?
48
+ @authorize_with = block
49
+ elsif !value.nil?
50
+ @authorize_with = value
51
+ else
52
+ @authorize_with
53
+ end
54
+ end
55
+
56
+ def authorize_with=(value)
57
+ @authorize_with = value
58
+ end
59
+
60
+ def authenticate_with(value = nil, &block)
61
+ if block_given?
62
+ @authenticate_with = block
63
+ elsif !value.nil?
64
+ @authenticate_with = value
65
+ else
66
+ @authenticate_with
67
+ end
68
+ end
69
+
70
+ def authenticate_with=(value)
71
+ @authenticate_with = value
72
+ end
73
+
74
+ def additional_sensitive_attributes=(value)
75
+ @additional_sensitive_attributes = Array(value)
76
+ SuperAdmin::SensitiveAttributes.reset!
77
+ end
78
+
79
+ def user_class_constant
80
+ user_class.is_a?(String) ? user_class.constantize : user_class
81
+ rescue NameError
82
+ raise ConfigurationError, "User class '#{user_class}' is not defined"
83
+ end
84
+
85
+ def parent_controller_constant
86
+ parent_controller.is_a?(String) ? parent_controller.constantize : parent_controller
87
+ rescue NameError
88
+ raise ConfigurationError, "Parent controller '#{parent_controller}' is not defined"
89
+ end
90
+ end
91
+
92
+ class << self
93
+ attr_writer :configuration
94
+
95
+ def configuration
96
+ @configuration ||= Configuration.new
97
+ end
98
+
99
+ def configure
100
+ yield(configuration)
101
+ end
102
+
103
+ def reset_configuration!
104
+ @configuration = Configuration.new
105
+ SuperAdmin::SensitiveAttributes.reset!
106
+ end
107
+
108
+ delegate :max_nested_depth,
109
+ :association_select_limit,
110
+ :association_pagination_limit,
111
+ to: :configuration
112
+
113
+ def enable_association_search?
114
+ configuration.enable_association_search
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,197 @@
1
+ en:
2
+ time:
3
+ formats:
4
+ long: "%B %d, %Y %H:%M"
5
+ super_admin:
6
+ layout:
7
+ window_title: "SuperAdmin - %{page_title}"
8
+ default_page_title: "Administration"
9
+ footer: "SuperAdmin - %{year}"
10
+ flash:
11
+ access_denied: "You do not have permission to access this section."
12
+ navigation:
13
+ brand: "SuperAdmin"
14
+ return_to_app: "Back to app"
15
+ dashboard: "Dashboard"
16
+ exports: "CSV Exports"
17
+ audit_logs: "Audit logs"
18
+ models_heading: "Models (%{count})"
19
+ dashboard:
20
+ index:
21
+ page_title: "Dashboard"
22
+ breadcrumb: "Dashboard"
23
+ heading: "SuperAdmin Dashboard"
24
+ intro: "Full management of every model in the application"
25
+ stats:
26
+ models_label: "Available models"
27
+ records_label: "Total records"
28
+ users_label: "Users"
29
+ table:
30
+ title: "All models"
31
+ headers:
32
+ model: "Model"
33
+ table: "Table"
34
+ records: "Records"
35
+ actions: "Actions"
36
+ manage: "Manage →"
37
+ resources:
38
+ shared:
39
+ search_placeholder: "Search..."
40
+ search_button: "Search"
41
+ reset_filters: "Reset"
42
+ advanced_filters: "Advanced filters"
43
+ contains_placeholder: "Contains..."
44
+ boolean_options:
45
+ placeholder: "—"
46
+ yes: "Yes"
47
+ no: "No"
48
+ min_placeholder: "Min"
49
+ max_placeholder: "Max"
50
+ clear_filters: "Clear filters"
51
+ apply_filters: "Apply"
52
+ bulk:
53
+ selected_suffix: "selected"
54
+ action_placeholder: "Bulk action"
55
+ destroy_action: "Delete selected"
56
+ apply: "Apply"
57
+ confirm: "Confirm the selected action on the checked items?"
58
+ select_all_label: "Select all items"
59
+ select_item_label: "Select item %{id}"
60
+ nested:
61
+ collection_hint: "Changes are saved when you submit the form."
62
+ add: "Add %{model}"
63
+ remove: "Remove"
64
+ remove_hint: "Tick to remove this entry when saving."
65
+ entry_existing: "%{model} #%{id}"
66
+ entry_new: "New %{model}"
67
+ missing_association: "Association %{name} is not available."
68
+ max_depth_exceeded: "Maximum nesting depth reached (%{max} levels). Cannot nest further."
69
+ index:
70
+ export_csv: "Export to CSV"
71
+ view_exports: "View exports"
72
+ new: "Create"
73
+ records_count:
74
+ zero: "%{total} records"
75
+ one: "%{total} record"
76
+ other: "%{total} records"
77
+ actions_header: "Actions"
78
+ view: "View"
79
+ edit: "Edit"
80
+ delete: "Delete"
81
+ delete_confirm: "Are you sure?"
82
+ empty_state: "No records match your search."
83
+ form:
84
+ error_heading:
85
+ one: "1 error prevented this record from being saved:"
86
+ other: "%{count} errors prevented this record from being saved:"
87
+ cancel: "Cancel"
88
+ create: "Create"
89
+ update: "Update"
90
+ boolean_hint: "Check to enable"
91
+ array_placeholder: "Enter one value per line or separate with commas"
92
+ association_limited: "Showing first %{count} results out of %{total} total. Use search to refine."
93
+ new:
94
+ page_title: "New - %{model}"
95
+ breadcrumb: "New"
96
+ heading: "Create %{model}"
97
+ edit:
98
+ page_title: "Edit - %{model}"
99
+ breadcrumb: "Edit"
100
+ heading: "Edit %{model} #%{id}"
101
+ show:
102
+ page_title: "Details - %{model}"
103
+ breadcrumb_id: "#%{id}"
104
+ heading: "%{model} #%{id}"
105
+ edit: "Edit"
106
+ delete: "Delete"
107
+ delete_confirm: "Are you sure you want to delete this record?"
108
+ attributes: "Attributes"
109
+ associations: "Associations"
110
+ association_count_html:
111
+ one: "%{number} record"
112
+ other: "%{number} records"
113
+ association_none: "None"
114
+ association_error: "Error: %{message}"
115
+ created_at: "Created on %{date}"
116
+ updated_at: "Updated on %{date}"
117
+ flash:
118
+ create:
119
+ success: "%{model} successfully created."
120
+ update:
121
+ success: "%{model} successfully updated."
122
+ destroy:
123
+ success: "%{model} successfully deleted."
124
+ dependencies: "Cannot delete: other records depend on this resource."
125
+ bulk:
126
+ success: "%{count} %{model} successfully deleted."
127
+ selection_required: "Select at least one record."
128
+ unsupported_action: "Unsupported bulk action."
129
+ dependencies: "Some records could not be deleted because of existing dependencies."
130
+ load_model_failed: "Model not found: %{resource}"
131
+ not_found: "%{model} not found."
132
+ exports:
133
+ index:
134
+ title: "CSV exports"
135
+ breadcrumb: "Exports"
136
+ heading: "CSV exports"
137
+ subtitle: "Track export progress and download them once ready."
138
+ back_to_resources: "Back to dashboard"
139
+ processing_hint: "Processing… You can leave this page."
140
+ expiration_hint: "Available until %{date}"
141
+ download: "Download"
142
+ pending: "Pending"
143
+ delete: "Delete"
144
+ delete_confirm: "Are you sure you want to delete this export?"
145
+ empty: "No exports generated yet."
146
+ table:
147
+ headers:
148
+ created_at: "Created at"
149
+ resource: "Resource"
150
+ status: "Status"
151
+ records: "Records"
152
+ actions: "Actions"
153
+ show:
154
+ title: "Export %{token}"
155
+ heading: "Export %{resource}"
156
+ download: "Download CSV"
157
+ created_at: "Created at"
158
+ status: "Status"
159
+ records: "Record count"
160
+ expires_at: "Expires at"
161
+ error: "Error"
162
+ status:
163
+ pending: "Pending"
164
+ processing: "Processing"
165
+ ready: "Ready"
166
+ failed: "Failed"
167
+ flash:
168
+ created: "Export started. Check the exports page to follow the progress (token %{token})."
169
+ unavailable: "The file is not available yet."
170
+ expired: "This file has expired and can no longer be downloaded."
171
+ not_found: "Export not found."
172
+ destroyed: "Export successfully deleted."
173
+ audit_logs:
174
+ title: "Activity log"
175
+ subtitle: "Track all administrative actions performed in SuperAdmin."
176
+ table_title: "Recent activity"
177
+ unknown_user: "Unknown user"
178
+ view_changes: "View changes"
179
+ filters:
180
+ search: "Search logs"
181
+ all_actions: "All actions"
182
+ all_resources: "All resources"
183
+ apply: "Filter"
184
+ headers:
185
+ performed_at: "Performed at"
186
+ user: "User"
187
+ resource: "Resource"
188
+ action: "Action"
189
+ changes: "Changes"
190
+ context: "Context"
191
+ empty: "No activity recorded yet."
192
+ missing_table: "Audit logs are not available yet. Run the SuperAdmin migrations to create the audit log table."
193
+ helpers:
194
+ resources:
195
+ empty_string: "(empty)"
196
+ actions:
197
+ close: "Close"