ruby_cms 0.1.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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/dhh.mdc +698 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +10 -0
  6. data/README.md +235 -0
  7. data/Rakefile +30 -0
  8. data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
  9. data/app/components/ruby_cms/admin/admin_page.rb +345 -0
  10. data/app/components/ruby_cms/admin/base_component.rb +78 -0
  11. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
  12. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
  13. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
  14. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
  15. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
  16. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
  17. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
  18. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
  19. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
  20. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
  21. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
  22. data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
  23. data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
  24. data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
  25. data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
  26. data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
  27. data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
  28. data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
  29. data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
  30. data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
  31. data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
  32. data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
  33. data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
  34. data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
  35. data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
  36. data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
  37. data/app/controllers/ruby_cms/errors_controller.rb +35 -0
  38. data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
  39. data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
  40. data/app/helpers/ruby_cms/application_helper.rb +41 -0
  41. data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
  42. data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
  43. data/app/helpers/ruby_cms/settings_helper.rb +160 -0
  44. data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
  45. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
  46. data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
  47. data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
  48. data/app/javascript/controllers/ruby_cms/index.js +104 -0
  49. data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
  50. data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
  51. data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
  52. data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
  53. data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
  54. data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
  55. data/app/models/concerns/content_block/publishable.rb +54 -0
  56. data/app/models/concerns/content_block/searchable.rb +22 -0
  57. data/app/models/content_block.rb +155 -0
  58. data/app/models/ruby_cms/content_block.rb +8 -0
  59. data/app/models/ruby_cms/permission.rb +28 -0
  60. data/app/models/ruby_cms/permittable.rb +39 -0
  61. data/app/models/ruby_cms/preference.rb +111 -0
  62. data/app/models/ruby_cms/user_permission.rb +12 -0
  63. data/app/models/ruby_cms/visitor_error.rb +109 -0
  64. data/app/services/ruby_cms/analytics/report.rb +362 -0
  65. data/app/services/ruby_cms/security_tracker.rb +92 -0
  66. data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
  67. data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
  68. data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
  69. data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
  70. data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
  71. data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
  72. data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
  73. data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
  74. data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
  75. data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
  76. data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
  77. data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
  78. data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
  79. data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
  80. data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
  81. data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
  82. data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
  83. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
  84. data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
  85. data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
  86. data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
  87. data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
  88. data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
  89. data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
  90. data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
  91. data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
  92. data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
  93. data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
  94. data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
  95. data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
  96. data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
  97. data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
  98. data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
  99. data/config/database.yml +6 -0
  100. data/config/importmap.rb +36 -0
  101. data/config/locales/en.yml +101 -0
  102. data/config/routes.rb +65 -0
  103. data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
  104. data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
  105. data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
  106. data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
  107. data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
  108. data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
  109. data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
  110. data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
  111. data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
  112. data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
  113. data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
  114. data/exe/ruby_cms +25 -0
  115. data/lib/generators/ruby_cms/install_generator.rb +1062 -0
  116. data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
  117. data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
  118. data/lib/ruby_cms/app_integration.rb +82 -0
  119. data/lib/ruby_cms/cli.rb +169 -0
  120. data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
  121. data/lib/ruby_cms/content_blocks_sync.rb +329 -0
  122. data/lib/ruby_cms/css_compiler.rb +35 -0
  123. data/lib/ruby_cms/engine.rb +498 -0
  124. data/lib/ruby_cms/settings.rb +145 -0
  125. data/lib/ruby_cms/settings_registry.rb +289 -0
  126. data/lib/ruby_cms/version.rb +5 -0
  127. data/lib/ruby_cms.rb +195 -0
  128. data/lib/tasks/ruby_cms.rake +27 -0
  129. data/log/test.log +17875 -0
  130. data/sig/ruby_cms.rbs +4 -0
  131. metadata +223 -0
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class SettingsController < BaseController
6
+ before_action { require_permission!(:manage_admin) }
7
+
8
+ def index
9
+ RubyCms::Settings.ensure_defaults!
10
+
11
+ @registry_entries = sorted_registry_entries
12
+ @categories = @registry_entries.map {|e| e.category.to_s }.uniq
13
+ @active_tab = resolve_active_tab(params[:tab], @categories)
14
+ @entries_for_tab = @registry_entries.select {|entry| entry.category.to_s == @active_tab }
15
+
16
+ @values = @entries_for_tab.to_h do |entry|
17
+ [entry.key, RubyCms::Settings.get(entry.key, default: entry.default)]
18
+ end
19
+ end
20
+
21
+ def update
22
+ updated_keys = apply_updates(extract_updates)
23
+ updated_keys = apply_nav_order_update(updated_keys)
24
+
25
+ respond_with_update_success(updated_keys)
26
+ rescue StandardError => e
27
+ respond_with_update_failure(e)
28
+ end
29
+
30
+ def reset_defaults
31
+ RubyCms::SettingsRegistry.seed_defaults!
32
+
33
+ RubyCms::SettingsRegistry.each do |entry|
34
+ RubyCms::Settings.set(entry.key, entry.default)
35
+ end
36
+
37
+ redirect_to ruby_cms_admin_settings_path(redirect_settings_params),
38
+ notice: t("ruby_cms.admin.settings.defaults_reset")
39
+ end
40
+
41
+ # Dedicated endpoint for saving nav order only. Reads raw JSON body; writes directly to preferences table.
42
+ def update_nav_order
43
+ order = nav_order_from_raw_body
44
+ unless order.kind_of?(Array) && order.any?
45
+ return render json: {
46
+ success: false,
47
+ error: "nav_order_main and nav_order_bottom required"
48
+ },
49
+ status: :unprocessable_content
50
+ end
51
+
52
+ rec = RubyCms::Preference.find_or_initialize_by(key: "nav_order")
53
+ rec.category = "navigation" if rec.new_record?
54
+ rec.value_type = "json"
55
+ rec.value = order.map(&:to_s).to_json
56
+ rec.save!
57
+ render json: { success: true, updated_keys: ["nav_order"], updated_count: 1 }
58
+ rescue StandardError => e
59
+ render json: { success: false, error: e.message }, status: :unprocessable_content
60
+ end
61
+
62
+ private
63
+
64
+ def nav_order_from_raw_body
65
+ request.body.rewind if request.body.respond_to?(:rewind)
66
+ body = request.body.read
67
+ return [] if body.blank?
68
+
69
+ data = JSON.parse(body)
70
+ nav_order_from_raw_hash(data)
71
+ rescue JSON::ParserError
72
+ []
73
+ end
74
+
75
+ def redirect_settings_params
76
+ { tab: params[:tab].presence || default_tab }.tap do |h|
77
+ h[:nav_sub] = params[:nav_sub].presence if params[:tab].to_s == "navigation"
78
+ end
79
+ end
80
+
81
+ # Persist nav order so it survives reload. Uses Preference directly so we hit the same row Settings.get reads.
82
+ def persist_nav_order(order)
83
+ return unless order.kind_of?(Array) && order.any?
84
+
85
+ RubyCms::Preference.set("nav_order", order)
86
+ end
87
+
88
+ # For JSON PATCH, Rails may wrap body under :settings (or leave at root). Body stream can be
89
+ # consumed by the parser so we try params first, then rewind and read raw body.
90
+ def nav_order_arrays_from_request
91
+ main = nav_order_param(:nav_order_main)
92
+ bottom = nav_order_param(:nav_order_bottom)
93
+ if main.nil? && bottom.nil? && request.content_mime_type&.symbol == :json
94
+ data = parsed_json_body
95
+ main, bottom = nav_order_arrays_from_json(data) if data
96
+ end
97
+ [
98
+ main.kind_of?(Array) ? main.map(&:to_s) : Array(main).map(&:to_s),
99
+ bottom.kind_of?(Array) ? bottom.map(&:to_s) : Array(bottom).map(&:to_s)
100
+ ]
101
+ end
102
+
103
+ def nav_order_param(key)
104
+ key_s = key.to_s
105
+ # Root (symbol or string)
106
+ params[key].presence || params[key_s].presence ||
107
+ # Common Rails JSON wrapper
108
+ params.dig(:settings, key).presence || params.dig(:settings, key_s).presence ||
109
+ params.dig("settings", key).presence || params.dig("settings", key_s).presence
110
+ end
111
+
112
+ def parsed_json_body
113
+ body = nil
114
+ if request.body.respond_to?(:rewind)
115
+ request.body.rewind
116
+ body = request.body.read
117
+ end
118
+ body = request.raw_post if body.blank? && body != false
119
+ return nil if body.blank?
120
+
121
+ JSON.parse(body)
122
+ rescue JSON::ParserError
123
+ nil
124
+ end
125
+
126
+ def sorted_registry_entries
127
+ RubyCms::SettingsRegistry
128
+ .entries
129
+ .values
130
+ .sort_by {|entry| [entry.category.to_s, entry.key.to_s] }
131
+ end
132
+
133
+ def resolve_active_tab(tab_param, categories)
134
+ requested = tab_param.to_s
135
+ return requested if categories.include?(requested)
136
+
137
+ categories.first || "general"
138
+ end
139
+
140
+ def default_tab
141
+ sorted_registry_entries.first&.category || "general"
142
+ end
143
+
144
+ def extract_updates
145
+ if params[:preferences].present?
146
+ params.require(:preferences).to_unsafe_h
147
+ elsif params[:key].present?
148
+ { params[:key].to_s => params[:value] }
149
+ else
150
+ {}
151
+ end
152
+ end
153
+
154
+ def apply_updates(updates)
155
+ updates.filter_map do |key, value|
156
+ entry = RubyCms::SettingsRegistry.fetch(key)
157
+ next unless entry
158
+
159
+ RubyCms::Settings.set(entry.key, value)
160
+ entry.key
161
+ end
162
+ end
163
+
164
+ def apply_nav_order_update(updated_keys)
165
+ nav_main, nav_bottom = nav_order_arrays_from_request
166
+ return updated_keys if nav_main.blank? && nav_bottom.blank?
167
+
168
+ order = (nav_main + nav_bottom).map(&:to_s)
169
+ persist_nav_order(order)
170
+ updated_keys + ["nav_order"]
171
+ end
172
+
173
+ def respond_with_update_success(updated_keys)
174
+ respond_to do |format|
175
+ format.html do
176
+ redirect_to ruby_cms_admin_settings_path(redirect_settings_params),
177
+ notice: t("ruby_cms.admin.settings.updated_many",
178
+ default: "#{updated_keys.size} setting(s) updated.")
179
+ end
180
+
181
+ format.json do
182
+ render json: {
183
+ success: true,
184
+ updated_keys: updated_keys,
185
+ updated_count: updated_keys.size
186
+ }
187
+ end
188
+ end
189
+ end
190
+
191
+ def respond_with_update_failure(error)
192
+ respond_to do |format|
193
+ format.html do
194
+ redirect_to ruby_cms_admin_settings_path(redirect_settings_params),
195
+ alert: error.message
196
+ end
197
+
198
+ format.json do
199
+ render json: { success: false, error: error.message }, status: :unprocessable_content
200
+ end
201
+ end
202
+ end
203
+
204
+ def nav_order_from_raw_hash(data)
205
+ main = data["nav_order_main"]
206
+ bottom = data["nav_order_bottom"]
207
+ main = Array(main).map(&:to_s) if main
208
+ bottom = Array(bottom).map(&:to_s) if bottom
209
+ (main || []) + (bottom || [])
210
+ end
211
+
212
+ def nav_order_arrays_from_json(data)
213
+ main = data.dig("settings", "nav_order_main") ||
214
+ data["nav_order_main"].presence ||
215
+ data[:nav_order_main].presence
216
+ bottom = data.dig("settings", "nav_order_bottom") ||
217
+ data["nav_order_bottom"].presence ||
218
+ data[:nav_order_bottom].presence
219
+ [main, bottom]
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class UserPermissionsController < BaseController
6
+ before_action { require_permission!(:manage_permissions) }
7
+ before_action :set_user
8
+
9
+ def index
10
+ @permissions = RubyCms::Permission.order(:key)
11
+ @user_permissions = if @user
12
+ RubyCms::UserPermission.where(user: @user)
13
+ .includes(:permission)
14
+ else
15
+ []
16
+ end
17
+ end
18
+
19
+ def create
20
+ permission = RubyCms::Permission.find(params[:permission_id])
21
+ if RubyCms::UserPermission.find_or_create_by!(user: @user, permission: permission)
22
+ redirect_to ruby_cms_admin_user_permissions_path(@user),
23
+ notice: t("ruby_cms.admin.user_permissions.granted")
24
+ end
25
+ rescue ActiveRecord::RecordInvalid
26
+ redirect_to ruby_cms_admin_user_permissions_path(@user),
27
+ alert: t("ruby_cms.admin.user_permissions.could_not_grant")
28
+ end
29
+
30
+ def destroy
31
+ up = RubyCms::UserPermission.find_by!(user: @user, id: params[:id])
32
+ up.destroy
33
+ redirect_to ruby_cms_admin_user_permissions_path(@user),
34
+ notice: t("ruby_cms.admin.user_permissions.revoked")
35
+ end
36
+
37
+ def bulk_delete
38
+ ids = Array(params[:item_ids]).filter_map(&:to_i)
39
+ user_permissions = RubyCms::UserPermission.where(user: @user, id: ids)
40
+ count = user_permissions.count
41
+ user_permissions.destroy_all
42
+ redirect_to ruby_cms_admin_user_permissions_path(@user),
43
+ notice: "#{count} permission(s) #{
44
+ t('ruby_cms.admin.user_permissions.revoked')
45
+ }."
46
+ end
47
+
48
+ private
49
+
50
+ def set_user
51
+ @user = user_class.find(params[:user_id])
52
+ end
53
+
54
+ def user_class
55
+ Object.const_get(Rails.application.config.ruby_cms.user_class_name.presence || "User")
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class UsersController < BaseController
6
+ include RubyCms::AdminPagination
7
+ include RubyCms::AdminTurboTable
8
+
9
+ paginates per_page: -> { RubyCms::Preference.get(:users_per_page, default: 50) },
10
+ turbo_frame: "admin_table_content"
11
+
12
+ before_action { require_permission!(:manage_permissions) }
13
+
14
+ def index
15
+ @users = paginated_users
16
+ @index ||= user_class.none
17
+ end
18
+
19
+ def create
20
+ user = user_class.new(user_params)
21
+ if user.save
22
+ redirect_to ruby_cms_admin_users_path, notice: t("ruby_cms.admin.users.created")
23
+ else
24
+ handle_create_failure(user)
25
+ end
26
+ end
27
+
28
+ def destroy
29
+ user = user_class.find(params[:id])
30
+ user.destroy
31
+ redirect_to ruby_cms_admin_users_path, notice: t("ruby_cms.admin.users.deleted")
32
+ end
33
+
34
+ def bulk_delete
35
+ ids = Array(params[:item_ids]).filter_map(&:to_i)
36
+ users = user_class.where(id: ids)
37
+ count = users.count
38
+ users.destroy_all
39
+ turbo_redirect_to ruby_cms_admin_users_path, notice: "#{count} user(s) #{
40
+ t('ruby_cms.admin.users.deleted')
41
+ }."
42
+ end
43
+
44
+ private
45
+
46
+ def paginated_users
47
+ collection = user_class.order(:id)
48
+ collection = apply_search_filter(collection)
49
+ paginate_collection(collection)
50
+ end
51
+
52
+ def apply_search_filter(collection)
53
+ return collection if params[:q].blank?
54
+
55
+ collection.where(build_search_conditions)
56
+ end
57
+
58
+ def build_search_conditions
59
+ search_term = "%#{params[:q].downcase}%"
60
+ email_attr = user_email_column
61
+ conditions = ["LOWER(#{email_attr}) LIKE ?"]
62
+ values = [search_term]
63
+
64
+ if numeric_query?
65
+ conditions << "id = ?"
66
+ values << params[:q].to_i
67
+ end
68
+
69
+ [conditions.join(" OR "), *values]
70
+ end
71
+
72
+ def user_email_column
73
+ user_class.column_names.include?("email_address") ? :email_address : :email
74
+ end
75
+
76
+ def numeric_query?
77
+ params[:q] =~ /^\d+$/
78
+ end
79
+
80
+ def handle_create_failure(user)
81
+ @users = user_class.order(:id).limit(100)
82
+ flash.now[:alert] = t(
83
+ "ruby_cms.admin.users.could_not_create",
84
+ errors: user.errors.full_messages.to_sentence
85
+ )
86
+ render :index, status: :unprocessable_content
87
+ end
88
+
89
+ def user_class
90
+ Object.const_get(Rails.application.config.ruby_cms.user_class_name.presence || "User")
91
+ end
92
+
93
+ def user_params
94
+ email_attr = user_class.column_names.include?("email_address") ? :email_address : :email
95
+ password_attrs = if user_class.column_names.include?("password")
96
+ %i[
97
+ password
98
+ password_confirmation
99
+ ]
100
+ else
101
+ []
102
+ end
103
+ params.expect(user: [email_attr, *password_attrs])
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ class VisitorErrorsController < BaseController
6
+ include RubyCms::AdminPagination
7
+ include RubyCms::AdminTurboTable
8
+
9
+ paginates per_page: -> { RubyCms::Preference.get(:visitor_errors_per_page, default: 25) },
10
+ turbo_frame: "admin_table_content"
11
+
12
+ before_action { require_permission!(:manage_visitor_errors) }
13
+ before_action :set_visitor_error, only: %i[show resolve]
14
+
15
+ def index
16
+ scope = RubyCms::VisitorError.recent
17
+ scope = scope.where(resolved: params[:resolved] == "true") if params[:resolved].present?
18
+ scope = apply_search_filter(scope)
19
+ scope = apply_error_type_filter(scope)
20
+ @visitor_errors = set_pagination_vars(scope)
21
+ render_index
22
+ end
23
+
24
+ def show; end
25
+
26
+ def resolve
27
+ @visitor_error.update!(resolved: true)
28
+ redirect_to ruby_cms_admin_visitor_errors_path,
29
+ notice: t("ruby_cms.admin.visitor_errors.resolved")
30
+ end
31
+
32
+ def bulk_delete
33
+ ids = Array(params[:item_ids]).filter_map(&:to_i)
34
+ count = RubyCms::VisitorError.where(id: ids).destroy_all.size
35
+ redirect_to ruby_cms_admin_visitor_errors_path,
36
+ notice: t("ruby_cms.admin.visitor_errors.bulk_deleted", count:)
37
+ end
38
+
39
+ def bulk_mark_as_resolved
40
+ ids = Array(params[:item_ids]).filter_map(&:to_i)
41
+ count = mark_visitor_errors_resolved(ids)
42
+ redirect_to ruby_cms_admin_visitor_errors_path,
43
+ notice: t("ruby_cms.admin.visitor_errors.bulk_resolved", count:)
44
+ end
45
+
46
+ private
47
+
48
+ def set_visitor_error
49
+ @visitor_error = RubyCms::VisitorError.find(params[:id])
50
+ end
51
+
52
+ def apply_search_filter(scope)
53
+ return scope if params[:search].blank?
54
+
55
+ term = sanitize_search_term(params[:search])
56
+ scope.where("request_path ILIKE ?", "%#{term}%")
57
+ end
58
+
59
+ def apply_error_type_filter(scope)
60
+ return scope if params[:error_type].blank?
61
+
62
+ term = sanitize_search_term(params[:error_type])
63
+ scope.where("error_class ILIKE ?", "%#{term}%")
64
+ end
65
+
66
+ def sanitize_search_term(term)
67
+ term.to_s.strip.gsub(/[%_\\]/, "").truncate(100)
68
+ end
69
+
70
+ def mark_visitor_errors_resolved(ids)
71
+ scope = RubyCms::VisitorError.where(id: ids)
72
+ count = 0
73
+ scope.find_each do |record|
74
+ record.update!(resolved: true)
75
+ count += 1
76
+ end
77
+ count
78
+ end
79
+
80
+ def render_index
81
+ if turbo_frame_request?
82
+ render :index, layout: false
83
+ else
84
+ render :index
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end