easy-admin-rails 0.2.5 → 0.2.7
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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +95 -0
- data/app/assets/builds/easy_admin.base.js.map +3 -3
- data/app/assets/builds/easy_admin.css +226 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
- data/app/components/easy_admin/form_layout_component.rb +553 -0
- data/app/components/easy_admin/navbar_component.rb +19 -4
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
- data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
- data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
- data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
- data/app/components/easy_admin/profile/settings_component.rb +103 -0
- data/app/components/easy_admin/show_layout_component.rb +694 -24
- data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
- data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
- data/app/components/easy_admin/two_factor/status_component.rb +92 -0
- data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
- data/app/controllers/easy_admin/application_controller.rb +10 -5
- data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
- data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
- data/app/controllers/easy_admin/profile_controller.rb +25 -0
- data/app/controllers/easy_admin/resources_controller.rb +1 -5
- data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
- data/app/controllers/easy_admin/sessions_controller.rb +107 -1
- data/app/helpers/easy_admin/fields_helper.rb +8 -22
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
- data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
- data/app/javascript/easy_admin/controllers.js +3 -1
- data/app/models/easy_admin/admin_user.rb +3 -0
- data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/change_password.html.erb +24 -0
- data/app/views/easy_admin/profile/index.html.erb +1 -0
- data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
- data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
- data/app/views/easy_admin/resources/edit.html.erb +2 -2
- data/app/views/easy_admin/resources/new.html.erb +2 -2
- data/app/views/easy_admin/resources/show.html.erb +3 -1
- data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
- data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
- data/config/routes.rb +20 -1
- data/lib/easy-admin-rails.rb +1 -0
- data/lib/easy_admin/field.rb +3 -2
- data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
- data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
- data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
- data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
- data/lib/easy_admin/layouts/dsl.rb +200 -0
- data/lib/easy_admin/layouts/layout_context.rb +189 -0
- data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
- data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
- data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
- data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
- data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
- data/lib/easy_admin/layouts/nodes/root.rb +25 -0
- data/lib/easy_admin/layouts/nodes/section.rb +46 -0
- data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
- data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
- data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
- data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
- data/lib/easy_admin/layouts.rb +28 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
- data/lib/easy_admin/resource/base.rb +2 -2
- data/lib/easy_admin/resource/dsl.rb +2 -11
- data/lib/easy_admin/resource/field_registry.rb +58 -2
- data/lib/easy_admin/resource.rb +0 -9
- data/lib/easy_admin/resource_modules.rb +21 -4
- data/lib/easy_admin/two_factor_authentication.rb +156 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
- data/lib/generators/easy_admin/two_factor/templates/README +29 -0
- data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
- data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
- metadata +49 -9
- data/lib/easy_admin/resource/form_builder.rb +0 -123
- data/lib/easy_admin/resource/layout_builder.rb +0 -249
- data/lib/easy_admin/resource/show_builder.rb +0 -359
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
- data/lib/generators/easy_admin/rbac/templates/super_admin.rb +0 -34
@@ -1,359 +0,0 @@
|
|
1
|
-
module EasyAdmin
|
2
|
-
module ResourceModules
|
3
|
-
# ShowBuilder class for show layout DSL
|
4
|
-
# Handles all show page layout definitions with rows, columns, cards, and tabs
|
5
|
-
class ShowBuilder
|
6
|
-
PREDEFINED_COLORS = %w[primary secondary success danger warning info light dark white transparent].freeze
|
7
|
-
COLUMN_SIZES = [1, 2, 3, 4, 6, 8, 12].freeze
|
8
|
-
SPACING_OPTIONS = %w[none small medium large].freeze
|
9
|
-
PADDING_OPTIONS = %w[none small medium large].freeze
|
10
|
-
HEADING_SIZES = %w[small medium large].freeze
|
11
|
-
DIVIDER_STYLES = %w[default dashed dotted thick].freeze
|
12
|
-
SHADOW_OPTIONS = %w[none small medium large].freeze
|
13
|
-
|
14
|
-
def initialize(resource_class)
|
15
|
-
@resource_class = resource_class
|
16
|
-
@context_stack = []
|
17
|
-
@current_row = nil
|
18
|
-
@current_column = nil
|
19
|
-
@current_card = nil
|
20
|
-
@current_tab_container = nil
|
21
|
-
@current_tab = nil
|
22
|
-
end
|
23
|
-
|
24
|
-
# Layout methods
|
25
|
-
def row(columns: 1, spacing: "medium", &block)
|
26
|
-
validate_spacing!(spacing)
|
27
|
-
|
28
|
-
row_config = {
|
29
|
-
type: :row,
|
30
|
-
columns_count: columns,
|
31
|
-
spacing: spacing,
|
32
|
-
columns: []
|
33
|
-
}
|
34
|
-
|
35
|
-
push_context
|
36
|
-
@current_row = row_config
|
37
|
-
|
38
|
-
if @current_column
|
39
|
-
@current_column[:elements] << row_config
|
40
|
-
else
|
41
|
-
@resource_class.add_show_layout_element(row_config)
|
42
|
-
end
|
43
|
-
|
44
|
-
instance_eval(&block) if block_given?
|
45
|
-
pop_context
|
46
|
-
end
|
47
|
-
|
48
|
-
def column(size: nil, &block)
|
49
|
-
raise "column must be called within a row block" unless @current_row
|
50
|
-
|
51
|
-
calculated_size = size || (12 / [@current_row[:columns_count], 1].max)
|
52
|
-
validate_column_size!(calculated_size)
|
53
|
-
|
54
|
-
column_config = {
|
55
|
-
type: :column,
|
56
|
-
size: calculated_size,
|
57
|
-
elements: []
|
58
|
-
}
|
59
|
-
|
60
|
-
push_context
|
61
|
-
@current_column = column_config
|
62
|
-
@current_row[:columns] << column_config
|
63
|
-
|
64
|
-
instance_eval(&block) if block_given?
|
65
|
-
pop_context
|
66
|
-
end
|
67
|
-
|
68
|
-
# Card methods
|
69
|
-
def card(title: nil, color: "light", padding: "medium", shadow: "small", &block)
|
70
|
-
raise "card must be called within a column block" unless @current_column
|
71
|
-
validate_color!(color)
|
72
|
-
validate_padding!(padding)
|
73
|
-
validate_shadow!(shadow)
|
74
|
-
|
75
|
-
card_config = {
|
76
|
-
type: :card,
|
77
|
-
title: title,
|
78
|
-
color: color,
|
79
|
-
padding: padding,
|
80
|
-
shadow: shadow,
|
81
|
-
elements: []
|
82
|
-
}
|
83
|
-
|
84
|
-
push_context
|
85
|
-
@current_card = card_config
|
86
|
-
@current_column[:elements] << card_config
|
87
|
-
|
88
|
-
instance_eval(&block) if block_given?
|
89
|
-
pop_context
|
90
|
-
end
|
91
|
-
|
92
|
-
def metric_card(title:, value:, color: "primary", icon: nil, trend: nil)
|
93
|
-
raise "metric_card must be called within a column block" unless @current_column
|
94
|
-
validate_color!(color)
|
95
|
-
|
96
|
-
metric_config = {
|
97
|
-
type: :metric_card,
|
98
|
-
title: title,
|
99
|
-
value: value,
|
100
|
-
icon: icon,
|
101
|
-
color: color,
|
102
|
-
trend: trend
|
103
|
-
}
|
104
|
-
|
105
|
-
@current_column[:elements] << metric_config
|
106
|
-
end
|
107
|
-
|
108
|
-
def chart_card(title:, chart_type:, **options, &block)
|
109
|
-
raise "chart_card must be called within a column block" unless @current_column
|
110
|
-
|
111
|
-
chart_config = {
|
112
|
-
type: :chart_card,
|
113
|
-
title: title,
|
114
|
-
chart_type: chart_type,
|
115
|
-
height: options[:height] || 350,
|
116
|
-
data_source: options[:data_source],
|
117
|
-
css_classes: options[:class] || "card",
|
118
|
-
config: {}
|
119
|
-
}
|
120
|
-
|
121
|
-
if block_given?
|
122
|
-
chart_builder = ChartBuilder.new
|
123
|
-
chart_builder.instance_eval(&block)
|
124
|
-
chart_config[:config] = chart_builder.config
|
125
|
-
end
|
126
|
-
|
127
|
-
@current_column[:elements] << chart_config
|
128
|
-
end
|
129
|
-
|
130
|
-
# Tab methods
|
131
|
-
def tabs(**options, &block)
|
132
|
-
container = @current_card || @current_column
|
133
|
-
raise "tabs must be called within a card or column block" unless container
|
134
|
-
|
135
|
-
tabs_config = {
|
136
|
-
type: :tabs,
|
137
|
-
css_classes: options[:class] || "tabs-menu",
|
138
|
-
tabs: []
|
139
|
-
}
|
140
|
-
|
141
|
-
push_context
|
142
|
-
@current_tab_container = tabs_config
|
143
|
-
container[:elements] << tabs_config
|
144
|
-
|
145
|
-
instance_eval(&block) if block_given?
|
146
|
-
pop_context
|
147
|
-
end
|
148
|
-
|
149
|
-
def tab(name, **options, &block)
|
150
|
-
raise "tab must be called within a tabs block" unless @current_tab_container
|
151
|
-
|
152
|
-
tab_config = {
|
153
|
-
name: name,
|
154
|
-
label: options[:label] || name,
|
155
|
-
icon: options[:icon],
|
156
|
-
active: options[:active] || false,
|
157
|
-
elements: []
|
158
|
-
}
|
159
|
-
|
160
|
-
push_context
|
161
|
-
@current_tab = tab_config
|
162
|
-
@current_tab_container[:tabs] << tab_config
|
163
|
-
|
164
|
-
instance_eval(&block) if block_given?
|
165
|
-
pop_context
|
166
|
-
end
|
167
|
-
|
168
|
-
# Field display methods
|
169
|
-
def field(name, **options)
|
170
|
-
container = @current_tab || @current_card || @current_column
|
171
|
-
raise "field must be called within a tab, card, or column block" unless container
|
172
|
-
|
173
|
-
label_value = if options.key?(:label) && options[:label] == false
|
174
|
-
nil
|
175
|
-
else
|
176
|
-
options[:label] || name.to_s.humanize
|
177
|
-
end
|
178
|
-
|
179
|
-
field_config = {
|
180
|
-
type: :field,
|
181
|
-
name: name,
|
182
|
-
label: label_value,
|
183
|
-
field_type: options[:field_type] || :default,
|
184
|
-
format: options[:format],
|
185
|
-
css_classes: options[:class],
|
186
|
-
show_label: options.fetch(:show_label, true)
|
187
|
-
}
|
188
|
-
|
189
|
-
container[:elements] << field_config
|
190
|
-
end
|
191
|
-
|
192
|
-
def fields(*field_names, **options)
|
193
|
-
field_names.each do |name|
|
194
|
-
field(name, **options)
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# Component rendering support with context
|
199
|
-
def render(component_class_or_instance, **options)
|
200
|
-
container = @current_tab || @current_card || @current_column
|
201
|
-
raise "render must be called within a tab, card, or column block" unless container
|
202
|
-
|
203
|
-
component_config = {
|
204
|
-
type: :custom_component,
|
205
|
-
component_class: component_class_or_instance.class,
|
206
|
-
component_options: options,
|
207
|
-
component_instance: component_class_or_instance
|
208
|
-
}
|
209
|
-
|
210
|
-
container[:elements] << component_config
|
211
|
-
component_class_or_instance
|
212
|
-
end
|
213
|
-
|
214
|
-
# Content methods
|
215
|
-
def content(html = nil, **options, &block)
|
216
|
-
container = @current_tab || @current_card || @current_column
|
217
|
-
raise "content must be called within a tab, card, or column block" unless container
|
218
|
-
|
219
|
-
content_config = {
|
220
|
-
type: :content,
|
221
|
-
html: html,
|
222
|
-
css_classes: options[:class],
|
223
|
-
block: block
|
224
|
-
}
|
225
|
-
|
226
|
-
container[:elements] << content_config
|
227
|
-
end
|
228
|
-
|
229
|
-
def heading(text, level: 2, size: "medium")
|
230
|
-
container = @current_tab || @current_card || @current_column
|
231
|
-
raise "heading must be called within a tab, card, or column block" unless container
|
232
|
-
|
233
|
-
validate_heading_level!(level)
|
234
|
-
validate_heading_size!(size)
|
235
|
-
|
236
|
-
heading_config = {
|
237
|
-
type: :heading,
|
238
|
-
text: text,
|
239
|
-
level: level,
|
240
|
-
size: size
|
241
|
-
}
|
242
|
-
|
243
|
-
container[:elements] << heading_config
|
244
|
-
end
|
245
|
-
|
246
|
-
def divider(style: "default")
|
247
|
-
container = @current_tab || @current_card || @current_column
|
248
|
-
raise "divider must be called within a tab, card, or column block" unless container
|
249
|
-
|
250
|
-
validate_divider_style!(style)
|
251
|
-
|
252
|
-
divider_config = {
|
253
|
-
type: :divider,
|
254
|
-
style: style
|
255
|
-
}
|
256
|
-
|
257
|
-
container[:elements] << divider_config
|
258
|
-
end
|
259
|
-
|
260
|
-
private
|
261
|
-
|
262
|
-
def validate_color!(color)
|
263
|
-
return if PREDEFINED_COLORS.include?(color.to_s)
|
264
|
-
raise ArgumentError, "Invalid color '#{color}'. Available colors: #{PREDEFINED_COLORS.join(', ')}"
|
265
|
-
end
|
266
|
-
|
267
|
-
def validate_column_size!(size)
|
268
|
-
return if COLUMN_SIZES.include?(size)
|
269
|
-
raise ArgumentError, "Invalid column size '#{size}'. Available sizes: #{COLUMN_SIZES.join(', ')}"
|
270
|
-
end
|
271
|
-
|
272
|
-
def validate_spacing!(spacing)
|
273
|
-
return if SPACING_OPTIONS.include?(spacing.to_s)
|
274
|
-
raise ArgumentError, "Invalid spacing '#{spacing}'. Available options: #{SPACING_OPTIONS.join(', ')}"
|
275
|
-
end
|
276
|
-
|
277
|
-
def validate_padding!(padding)
|
278
|
-
return if PADDING_OPTIONS.include?(padding.to_s)
|
279
|
-
raise ArgumentError, "Invalid padding '#{padding}'. Available options: #{PADDING_OPTIONS.join(', ')}"
|
280
|
-
end
|
281
|
-
|
282
|
-
def validate_shadow!(shadow)
|
283
|
-
return if SHADOW_OPTIONS.include?(shadow.to_s)
|
284
|
-
raise ArgumentError, "Invalid shadow '#{shadow}'. Available options: #{SHADOW_OPTIONS.join(', ')}"
|
285
|
-
end
|
286
|
-
|
287
|
-
def validate_heading_level!(level)
|
288
|
-
return if (1..6).include?(level)
|
289
|
-
raise ArgumentError, "Invalid heading level '#{level}'. Must be between 1 and 6"
|
290
|
-
end
|
291
|
-
|
292
|
-
def validate_heading_size!(size)
|
293
|
-
return if HEADING_SIZES.include?(size.to_s)
|
294
|
-
raise ArgumentError, "Invalid heading size '#{size}'. Available sizes: #{HEADING_SIZES.join(', ')}"
|
295
|
-
end
|
296
|
-
|
297
|
-
def validate_divider_style!(style)
|
298
|
-
return if DIVIDER_STYLES.include?(style.to_s)
|
299
|
-
raise ArgumentError, "Invalid divider style '#{style}'. Available styles: #{DIVIDER_STYLES.join(', ')}"
|
300
|
-
end
|
301
|
-
|
302
|
-
def push_context
|
303
|
-
@context_stack.push({
|
304
|
-
row: @current_row,
|
305
|
-
column: @current_column,
|
306
|
-
card: @current_card,
|
307
|
-
tab_container: @current_tab_container,
|
308
|
-
tab: @current_tab
|
309
|
-
})
|
310
|
-
end
|
311
|
-
|
312
|
-
def pop_context
|
313
|
-
if @context_stack.any?
|
314
|
-
context = @context_stack.pop
|
315
|
-
@current_row = context[:row]
|
316
|
-
@current_column = context[:column]
|
317
|
-
@current_card = context[:card]
|
318
|
-
@current_tab_container = context[:tab_container]
|
319
|
-
@current_tab = context[:tab]
|
320
|
-
else
|
321
|
-
@current_row = nil
|
322
|
-
@current_column = nil
|
323
|
-
@current_card = nil
|
324
|
-
@current_tab_container = nil
|
325
|
-
@current_tab = nil
|
326
|
-
end
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
# ChartBuilder class for chart configuration
|
331
|
-
class ChartBuilder
|
332
|
-
attr_reader :config
|
333
|
-
|
334
|
-
def initialize
|
335
|
-
@config = {}
|
336
|
-
end
|
337
|
-
|
338
|
-
def series(data)
|
339
|
-
@config[:series] = data
|
340
|
-
end
|
341
|
-
|
342
|
-
def categories(data)
|
343
|
-
@config[:categories] = data
|
344
|
-
end
|
345
|
-
|
346
|
-
def colors(data)
|
347
|
-
@config[:colors] = data
|
348
|
-
end
|
349
|
-
|
350
|
-
def title(text)
|
351
|
-
@config[:title] = text
|
352
|
-
end
|
353
|
-
|
354
|
-
def subtitle(text)
|
355
|
-
@config[:subtitle] = text
|
356
|
-
end
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
class UpdateUsersForEasyAdminPermissions < ActiveRecord::Migration<%= migration_version %>
|
2
|
-
def change
|
3
|
-
add_column :<%= user_model_name.tableize %>, :permissions_cache, :json, default: {}, null: false
|
4
|
-
add_index :<%= user_model_name.tableize %>, :permissions_cache, using: :gin
|
5
|
-
end
|
6
|
-
end
|
@@ -1,244 +0,0 @@
|
|
1
|
-
require 'rails/generators'
|
2
|
-
require 'rails/generators/migration'
|
3
|
-
|
4
|
-
module EasyAdmin
|
5
|
-
module Generators
|
6
|
-
class RbacGenerator < Rails::Generators::Base
|
7
|
-
include Rails::Generators::Migration
|
8
|
-
|
9
|
-
source_root File.expand_path('../templates', __FILE__)
|
10
|
-
|
11
|
-
desc 'Generate EasyAdmin Role-Based Access Control setup'
|
12
|
-
|
13
|
-
def self.next_migration_number(path)
|
14
|
-
Time.current.utc.strftime("%Y%m%d%H%M%S")
|
15
|
-
end
|
16
|
-
|
17
|
-
def copy_migration
|
18
|
-
migration_template(
|
19
|
-
'add_rbac_to_admin_users.rb',
|
20
|
-
'db/migrate/add_rbac_to_admin_users.rb',
|
21
|
-
migration_version: migration_version
|
22
|
-
)
|
23
|
-
end
|
24
|
-
|
25
|
-
def create_role_models
|
26
|
-
template 'super_admin.rb', 'app/models/easy_admin/super_admin.rb'
|
27
|
-
template 'admin.rb', 'app/models/easy_admin/admin.rb'
|
28
|
-
template 'editor.rb', 'app/models/easy_admin/editor.rb'
|
29
|
-
template 'viewer.rb', 'app/models/easy_admin/viewer.rb'
|
30
|
-
end
|
31
|
-
|
32
|
-
def update_admin_user_model
|
33
|
-
inject_into_file 'app/models/easy_admin/admin_user.rb', after: "self.table_name = \"easy_admin_admin_users\"\n" do
|
34
|
-
<<-RUBY
|
35
|
-
|
36
|
-
# STI configuration for role-based access
|
37
|
-
self.inheritance_column = :type
|
38
|
-
|
39
|
-
# Associations
|
40
|
-
has_many :audit_logs, class_name: 'EasyAdmin::AuditLog', foreign_key: :admin_user_id, dependent: :destroy
|
41
|
-
|
42
|
-
# Scopes
|
43
|
-
scope :by_role, ->(role) { where(type: "EasyAdmin::\#{role}") }
|
44
|
-
|
45
|
-
# Cache permissions per request
|
46
|
-
def permissions_cache
|
47
|
-
@permissions_cache ||= {}
|
48
|
-
end
|
49
|
-
|
50
|
-
# Core authorization methods (to be overridden by subclasses)
|
51
|
-
def can_access_resource?(resource_name)
|
52
|
-
accessible_resources.include?(resource_name.to_s)
|
53
|
-
end
|
54
|
-
|
55
|
-
def can_perform_action?(resource_name, action)
|
56
|
-
return false unless can_access_resource?(resource_name)
|
57
|
-
allowed_actions_for(resource_name).include?(action.to_s)
|
58
|
-
end
|
59
|
-
|
60
|
-
def can_access_field?(resource_name, field_name, action = :read)
|
61
|
-
return false unless can_access_resource?(resource_name)
|
62
|
-
!restricted_fields_for(resource_name, action).include?(field_name.to_s)
|
63
|
-
end
|
64
|
-
|
65
|
-
# Override in subclasses
|
66
|
-
def accessible_resources
|
67
|
-
[]
|
68
|
-
end
|
69
|
-
|
70
|
-
def allowed_actions_for(resource_name)
|
71
|
-
[:index, :show]
|
72
|
-
end
|
73
|
-
|
74
|
-
def restricted_fields_for(resource_name, action)
|
75
|
-
[]
|
76
|
-
end
|
77
|
-
|
78
|
-
def apply_resource_scope(resource_name, scope)
|
79
|
-
scope
|
80
|
-
end
|
81
|
-
|
82
|
-
# Helper for caching expensive operations
|
83
|
-
def cached_permission(key, &block)
|
84
|
-
permissions_cache[key] ||= block.call
|
85
|
-
end
|
86
|
-
RUBY
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def create_permission_cache
|
91
|
-
template 'permission_cache.rb', 'lib/easy_admin/permission_cache.rb'
|
92
|
-
end
|
93
|
-
|
94
|
-
def create_audit_log_model
|
95
|
-
template 'audit_log.rb', 'app/models/easy_admin/audit_log.rb'
|
96
|
-
|
97
|
-
migration_template(
|
98
|
-
'create_audit_logs.rb',
|
99
|
-
'db/migrate/create_easy_admin_audit_logs.rb',
|
100
|
-
migration_version: migration_version
|
101
|
-
)
|
102
|
-
end
|
103
|
-
|
104
|
-
def update_application_controller
|
105
|
-
inject_into_file 'app/controllers/easy_admin/application_controller.rb',
|
106
|
-
before: "include Pagy::Backend" do
|
107
|
-
<<-RUBY
|
108
|
-
class UnauthorizedError < StandardError; end
|
109
|
-
|
110
|
-
RUBY
|
111
|
-
end
|
112
|
-
|
113
|
-
inject_into_file 'app/controllers/easy_admin/application_controller.rb',
|
114
|
-
after: "before_action :authenticate_easy_admin_admin_user!\n" do
|
115
|
-
<<-RUBY
|
116
|
-
before_action :check_resource_access!, except: [:index]
|
117
|
-
|
118
|
-
rescue_from UnauthorizedError do |exception|
|
119
|
-
respond_to do |format|
|
120
|
-
format.html { redirect_to easy_admin.root_path, alert: 'You are not authorized to access this resource.' }
|
121
|
-
format.turbo_stream do
|
122
|
-
render turbo_stream: turbo_stream.replace("notifications",
|
123
|
-
EasyAdmin::NotificationComponent.new(
|
124
|
-
message: 'You are not authorized to access this resource',
|
125
|
-
type: :error
|
126
|
-
).call
|
127
|
-
)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
RUBY
|
132
|
-
end
|
133
|
-
|
134
|
-
inject_into_file 'app/controllers/easy_admin/application_controller.rb',
|
135
|
-
before: "def authenticate_admin_user!" do
|
136
|
-
<<-RUBY
|
137
|
-
def check_resource_access!
|
138
|
-
return unless defined?(@resource_class)
|
139
|
-
|
140
|
-
unless current_admin_user.can_access_resource?(@resource_class.resource_name)
|
141
|
-
raise UnauthorizedError, "Access denied to \#{@resource_class.resource_name}"
|
142
|
-
end
|
143
|
-
|
144
|
-
check_action_permission!
|
145
|
-
end
|
146
|
-
|
147
|
-
def check_action_permission!
|
148
|
-
action_map = {
|
149
|
-
'index' => :index,
|
150
|
-
'show' => :show,
|
151
|
-
'new' => :new,
|
152
|
-
'create' => :create,
|
153
|
-
'edit' => :edit,
|
154
|
-
'update' => :update,
|
155
|
-
'destroy' => :destroy,
|
156
|
-
'execute' => :batch_action
|
157
|
-
}
|
158
|
-
|
159
|
-
mapped_action = action_map[action_name] || action_name.to_sym
|
160
|
-
|
161
|
-
unless current_admin_user.can_perform_action?(@resource_class.resource_name, mapped_action)
|
162
|
-
raise UnauthorizedError, "Cannot perform \#{mapped_action} on \#{@resource_class.resource_name}"
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
def apply_role_scoping(scope)
|
167
|
-
current_admin_user.apply_resource_scope(@resource_class.resource_name, scope)
|
168
|
-
end
|
169
|
-
|
170
|
-
def permission_cache
|
171
|
-
@permission_cache ||= EasyAdmin::PermissionCache.new(current_admin_user)
|
172
|
-
end
|
173
|
-
|
174
|
-
RUBY
|
175
|
-
end
|
176
|
-
|
177
|
-
inject_into_file 'app/controllers/easy_admin/application_controller.rb',
|
178
|
-
after: "helper_method :current_admin_user, :admin_user_signed_in?" do
|
179
|
-
", :permission_cache"
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def create_seeds
|
184
|
-
create_file 'db/seeds/easy_admin_rbac.rb', <<~RUBY
|
185
|
-
# Create default admin users with different roles
|
186
|
-
if Rails.env.development?
|
187
|
-
# Super Admin - full access
|
188
|
-
EasyAdmin::SuperAdmin.find_or_create_by(email: 'super@example.com') do |admin|
|
189
|
-
admin.password = 'password'
|
190
|
-
admin.password_confirmation = 'password'
|
191
|
-
admin.first_name = 'Super'
|
192
|
-
admin.last_name = 'Admin'
|
193
|
-
admin.confirmed_at = Time.current
|
194
|
-
end
|
195
|
-
|
196
|
-
# Standard Admin - most access except system settings
|
197
|
-
EasyAdmin::Admin.find_or_create_by(email: 'admin@example.com') do |admin|
|
198
|
-
admin.password = 'password'
|
199
|
-
admin.password_confirmation = 'password'
|
200
|
-
admin.first_name = 'Admin'
|
201
|
-
admin.last_name = 'User'
|
202
|
-
admin.confirmed_at = Time.current
|
203
|
-
end
|
204
|
-
|
205
|
-
# Editor - content management only
|
206
|
-
EasyAdmin::Editor.find_or_create_by(email: 'editor@example.com') do |admin|
|
207
|
-
admin.password = 'password'
|
208
|
-
admin.password_confirmation = 'password'
|
209
|
-
admin.first_name = 'Content'
|
210
|
-
admin.last_name = 'Editor'
|
211
|
-
admin.confirmed_at = Time.current
|
212
|
-
end
|
213
|
-
|
214
|
-
# Viewer - read-only access
|
215
|
-
EasyAdmin::Viewer.find_or_create_by(email: 'viewer@example.com') do |admin|
|
216
|
-
admin.password = 'password'
|
217
|
-
admin.password_confirmation = 'password'
|
218
|
-
admin.first_name = 'Read'
|
219
|
-
admin.last_name = 'Only'
|
220
|
-
admin.confirmed_at = Time.current
|
221
|
-
end
|
222
|
-
|
223
|
-
puts "EasyAdmin RBAC users created!"
|
224
|
-
puts "Roles available:"
|
225
|
-
puts " SuperAdmin - super@example.com / password (full access)"
|
226
|
-
puts " Admin - admin@example.com / password (standard admin)"
|
227
|
-
puts " Editor - editor@example.com / password (content only)"
|
228
|
-
puts " Viewer - viewer@example.com / password (read-only)"
|
229
|
-
end
|
230
|
-
RUBY
|
231
|
-
end
|
232
|
-
|
233
|
-
def show_readme
|
234
|
-
readme 'RBAC_README'
|
235
|
-
end
|
236
|
-
|
237
|
-
private
|
238
|
-
|
239
|
-
def migration_version
|
240
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
241
|
-
end
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
class AddRbacToAdminUsers < ActiveRecord::Migration<%= migration_version %>
|
2
|
-
def change
|
3
|
-
# Add type column for STI
|
4
|
-
add_column :easy_admin_admin_users, :type, :string
|
5
|
-
add_column :easy_admin_admin_users, :permissions, :json, default: {}
|
6
|
-
add_column :easy_admin_admin_users, :resource_access, :json, default: {}
|
7
|
-
|
8
|
-
# Add indexes for performance
|
9
|
-
add_index :easy_admin_admin_users, :type
|
10
|
-
add_index :easy_admin_admin_users, [:type, :locked_at]
|
11
|
-
|
12
|
-
# Migrate existing users to Admin type
|
13
|
-
reversible do |dir|
|
14
|
-
dir.up do
|
15
|
-
execute <<-SQL
|
16
|
-
UPDATE easy_admin_admin_users
|
17
|
-
SET type = 'EasyAdmin::Admin'
|
18
|
-
WHERE type IS NULL
|
19
|
-
SQL
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
module EasyAdmin
|
2
|
-
class SuperAdmin < AdminUser
|
3
|
-
def accessible_resources
|
4
|
-
cached_permission(:resources) do
|
5
|
-
EasyAdmin::ResourceRegistry.all_resources.map(&:resource_name)
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
def allowed_actions_for(resource_name)
|
10
|
-
[:index, :show, :new, :create, :edit, :update, :destroy, :batch_action, :row_action].map(&:to_s)
|
11
|
-
end
|
12
|
-
|
13
|
-
def restricted_fields_for(resource_name, action)
|
14
|
-
[] # No restrictions for super admin
|
15
|
-
end
|
16
|
-
|
17
|
-
def apply_resource_scope(resource_name, scope)
|
18
|
-
scope # No filtering for super admin
|
19
|
-
end
|
20
|
-
|
21
|
-
# Super admin specific methods
|
22
|
-
def super_admin?
|
23
|
-
true
|
24
|
-
end
|
25
|
-
|
26
|
-
def role_name
|
27
|
-
'Super Admin'
|
28
|
-
end
|
29
|
-
|
30
|
-
def role_badge_color
|
31
|
-
'red'
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|