easy-admin-rails 0.1.14 → 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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +254 -18
  3. data/app/assets/builds/easy_admin.base.js.map +4 -4
  4. data/app/assets/builds/easy_admin.css +112 -18
  5. data/app/components/easy_admin/base_component.rb +1 -0
  6. data/app/components/easy_admin/form_tabs_component.rb +5 -2
  7. data/app/components/easy_admin/navbar_component.rb +5 -1
  8. data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
  9. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
  10. data/app/components/easy_admin/resources/index_component.rb +1 -4
  11. data/app/components/easy_admin/sidebar_component.rb +67 -2
  12. data/app/components/easy_admin/versions/diff_modal_component.rb +5 -1
  13. data/app/controllers/easy_admin/application_controller.rb +131 -1
  14. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  15. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  16. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  17. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  18. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  19. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  20. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  21. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  22. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  23. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  24. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  25. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  26. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  27. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  28. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  29. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  30. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  31. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  33. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  34. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  35. data/app/javascript/easy_admin/controllers.js +5 -1
  36. data/app/models/easy_admin/admin_user.rb +6 -0
  37. data/app/policies/admin_user_policy.rb +36 -0
  38. data/app/policies/application_policy.rb +83 -0
  39. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  40. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  41. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  42. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  43. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  44. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  46. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  47. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  48. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  49. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  50. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  51. data/app/views/easy_admin/resources/index.html.erb +1 -1
  52. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  53. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  54. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  55. data/config/initializers/easy_admin_permissions.rb +73 -0
  56. data/db/seeds/easy_admin_permissions.rb +121 -0
  57. data/lib/easy-admin-rails.rb +2 -0
  58. data/lib/easy_admin/permissions/component.rb +168 -0
  59. data/lib/easy_admin/permissions/configuration.rb +37 -0
  60. data/lib/easy_admin/permissions/controller.rb +164 -0
  61. data/lib/easy_admin/permissions/dsl.rb +180 -0
  62. data/lib/easy_admin/permissions/models.rb +44 -0
  63. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  64. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  65. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  66. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  67. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  68. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  69. data/lib/easy_admin/permissions.rb +113 -0
  70. data/lib/easy_admin/resource/base.rb +119 -0
  71. data/lib/easy_admin/resource/configuration.rb +148 -0
  72. data/lib/easy_admin/resource/dsl.rb +117 -0
  73. data/lib/easy_admin/resource/field_registry.rb +189 -0
  74. data/lib/easy_admin/resource/form_builder.rb +123 -0
  75. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  76. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  77. data/lib/easy_admin/resource/show_builder.rb +359 -0
  78. data/lib/easy_admin/resource.rb +8 -835
  79. data/lib/easy_admin/resource_modules.rb +11 -0
  80. data/lib/easy_admin/version.rb +1 -1
  81. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  82. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  84. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  90. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  91. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  92. metadata +62 -5
  93. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,121 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class PermissionDeniedComponent < EasyAdmin::BaseComponent
4
+ def initialize(permission:, user: nil, context: nil)
5
+ @permission = permission
6
+ @user = user
7
+ @context = context
8
+ end
9
+
10
+ def view_template
11
+ div(class: "min-h-96 flex items-center justify-center p-8") do
12
+ div(class: "text-center max-w-md mx-auto") do
13
+ # Icon
14
+ div(class: "mb-6") do
15
+ svg(class: "w-20 h-20 mx-auto text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
16
+ path(stroke_linecap: "round", stroke_linejoin: "round", stroke_width: "2",
17
+ d: "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z")
18
+ end
19
+ end
20
+
21
+ # Title
22
+ h2(class: "text-3xl font-bold text-gray-900 mb-4") { "Access Denied" }
23
+
24
+ # Description
25
+ div(class: "text-gray-600 mb-6 space-y-2") do
26
+ p { "You don't have permission to access this resource." }
27
+ if @permission
28
+ div(class: "text-sm bg-gray-100 px-3 py-2 rounded-lg font-mono") do
29
+ span(class: "text-gray-500") { "Required permission: " }
30
+ span(class: "text-red-600 font-semibold") { @permission }
31
+ end
32
+ end
33
+ if @context
34
+ div(class: "text-sm text-gray-500") do
35
+ span { "Context: " }
36
+ span(class: "font-medium") { @context.to_s }
37
+ end
38
+ end
39
+ end
40
+
41
+ # User info (if available)
42
+ if @user
43
+ div(class: "text-sm text-gray-500 mb-6 p-3 bg-gray-50 rounded-lg") do
44
+ p do
45
+ span { "Signed in as: " }
46
+ span(class: "font-medium text-gray-700") { @user.email || @user.name || "User ##{@user.id}" }
47
+ end
48
+ if @user.respond_to?(:roles) && @user.roles.any?
49
+ p(class: "mt-2") do
50
+ span { "Your roles: " }
51
+ @user.roles.active.each_with_index do |role, index|
52
+ span(class: "inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800 mr-1") do
53
+ role.name
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Actions
62
+ div(class: "space-y-3") do
63
+ # Go back button
64
+ button(
65
+ onclick: "history.back()",
66
+ class: "w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
67
+ ) do
68
+ "← Go Back"
69
+ end
70
+
71
+ # Contact admin link (if configured)
72
+ if support_contact_available?
73
+ a(
74
+ href: support_contact_url,
75
+ class: "inline-block text-sm text-blue-600 hover:text-blue-800 transition-colors"
76
+ ) do
77
+ "Contact administrator for access"
78
+ end
79
+ end
80
+ end
81
+
82
+ # Additional info
83
+ div(class: "mt-8 pt-6 border-t border-gray-200 text-xs text-gray-400") do
84
+ p { "If you believe this is an error, please contact your administrator." }
85
+ if Rails.env.development?
86
+ div(class: "mt-2 p-2 bg-yellow-50 rounded text-yellow-700 text-left font-mono text-xs") do
87
+ p { "Dev info:" }
88
+ p { "Permission: #{@permission}" }
89
+ p { "Context: #{@context}" } if @context
90
+ p { "User ID: #{@user&.id}" }
91
+ p { "Time: #{Time.current}" }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def support_contact_available?
102
+ # Check if there's a support contact configured
103
+ respond_to?(:admin_email) ||
104
+ defined?(Rails.application.config.admin_email) ||
105
+ defined?(EasyAdmin.configuration&.support_email)
106
+ end
107
+
108
+ def support_contact_url
109
+ if respond_to?(:admin_email)
110
+ "mailto:#{admin_email}?subject=Access%20Request&body=I%20need%20access%20to%20#{@permission}"
111
+ elsif defined?(Rails.application.config.admin_email)
112
+ "mailto:#{Rails.application.config.admin_email}?subject=Access%20Request"
113
+ elsif defined?(EasyAdmin.configuration&.support_email)
114
+ "mailto:#{EasyAdmin.configuration.support_email}?subject=Access%20Request"
115
+ else
116
+ "#"
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,231 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class ResourcePermissions
4
+ # Standard EasyAdmin actions that every resource supports
5
+ STANDARD_ACTIONS = %w[create read update delete].freeze
6
+
7
+ # Additional administrative actions
8
+ ADMIN_ACTIONS = %w[manage_versions batch_actions row_actions].freeze
9
+
10
+ # Legacy mapping for backwards compatibility
11
+ ACTION_MAPPING = {
12
+ 'index' => 'read',
13
+ 'show' => 'read',
14
+ 'new' => 'create',
15
+ 'edit' => 'update',
16
+ 'destroy' => 'delete'
17
+ }.freeze
18
+
19
+ class << self
20
+ # Discover all EasyAdmin resources and their permissions
21
+ def discover_all_permissions
22
+ discovered_permissions = []
23
+
24
+ all_resource_classes.each do |resource_class|
25
+ resource_name = extract_resource_name(resource_class)
26
+
27
+ # Add standard CRUD permissions
28
+ STANDARD_ACTIONS.each do |action|
29
+ discovered_permissions << {
30
+ name: "#{resource_name}:#{action}",
31
+ resource_type: resource_name,
32
+ action: action,
33
+ description: generate_permission_description(resource_name, action)
34
+ }
35
+ end
36
+
37
+ # Add administrative permissions
38
+ ADMIN_ACTIONS.each do |action|
39
+ discovered_permissions << {
40
+ name: "#{resource_name}:#{action}",
41
+ resource_type: resource_name,
42
+ action: action,
43
+ description: generate_permission_description(resource_name, action)
44
+ }
45
+ end
46
+
47
+ # Add any custom actions defined in the resource
48
+ custom_actions = extract_custom_actions(resource_class)
49
+ custom_actions.each do |action|
50
+ discovered_permissions << {
51
+ name: "#{resource_name}:#{action}",
52
+ resource_type: resource_name,
53
+ action: action,
54
+ description: generate_permission_description(resource_name, action)
55
+ }
56
+ end
57
+ end
58
+
59
+ discovered_permissions
60
+ end
61
+
62
+ # Get all available resource names
63
+ def available_resources
64
+ all_resource_classes.map { |resource_class| extract_resource_name(resource_class) }
65
+ end
66
+
67
+ # Get all actions for a specific resource
68
+ def actions_for_resource(resource_name)
69
+ resource_class = find_resource_class(resource_name)
70
+ return STANDARD_ACTIONS if resource_class.nil?
71
+
72
+ STANDARD_ACTIONS + extract_custom_actions(resource_class)
73
+ end
74
+
75
+ # Seed permissions into database
76
+ def seed_permissions!
77
+ Rails.logger.info "🔐 Seeding EasyAdmin resource permissions..."
78
+
79
+ permissions_data = discover_all_permissions
80
+ created_count = 0
81
+
82
+ permissions_data.each do |permission_data|
83
+ permission = EasyAdmin::Permissions::Permission.find_or_create_by(
84
+ name: permission_data[:name]
85
+ ) do |p|
86
+ p.resource_type = permission_data[:resource_type]
87
+ p.action = permission_data[:action]
88
+ p.description = permission_data[:description]
89
+ end
90
+
91
+ created_count += 1 if permission.saved_change_to_id?
92
+ end
93
+
94
+ Rails.logger.info "✅ Seeded #{created_count} new permissions (#{permissions_data.size} total)"
95
+ permissions_data.size
96
+ end
97
+
98
+ private
99
+
100
+ # Get all EasyAdmin resource classes
101
+ def all_resource_classes
102
+ # First try the ResourceRegistry if available
103
+ if defined?(EasyAdmin::ResourceRegistry)
104
+ registry_resources = EasyAdmin::ResourceRegistry.all_resources.values
105
+ return registry_resources if registry_resources.any?
106
+ end
107
+
108
+ # Fallback: scan for resource classes in the app
109
+ discover_resource_classes_from_filesystem
110
+ end
111
+
112
+ # Discover resource classes by scanning the filesystem
113
+ def discover_resource_classes_from_filesystem
114
+ resource_classes = []
115
+
116
+ # Check standard Rails app structure
117
+ resource_paths = [
118
+ Rails.root.join("app", "easy_admin", "resources"),
119
+ Rails.root.join("app", "admin", "resources")
120
+ ]
121
+
122
+ resource_paths.each do |path|
123
+ next unless path.exist?
124
+
125
+ Dir.glob(path.join("**", "*_resource.rb")).each do |file|
126
+ # Extract class name from file path
127
+ relative_path = file.gsub("#{path}/", "").gsub(".rb", "")
128
+ class_name = relative_path.camelize
129
+
130
+ begin
131
+ resource_class = class_name.constantize
132
+ resource_classes << resource_class if resource_class < EasyAdmin::Resource
133
+ rescue NameError => e
134
+ Rails.logger.warn "Could not load resource class #{class_name}: #{e.message}"
135
+ end
136
+ end
137
+ end
138
+
139
+ resource_classes
140
+ end
141
+
142
+ # Extract resource name from class (e.g., UserResource -> "user")
143
+ def extract_resource_name(resource_class)
144
+ resource_class.name.demodulize.gsub(/Resource$/, '').underscore
145
+ end
146
+
147
+ # Find resource class by name
148
+ def find_resource_class(resource_name)
149
+ class_name = "#{resource_name.camelize}Resource"
150
+
151
+ # Try direct constantize first
152
+ begin
153
+ return class_name.constantize
154
+ rescue NameError
155
+ # Not found
156
+ end
157
+
158
+ # Try with common namespaces
159
+ ["", "Admin::", "EasyAdmin::"].each do |namespace|
160
+ begin
161
+ full_class_name = "#{namespace}#{class_name}"
162
+ return full_class_name.constantize
163
+ rescue NameError
164
+ next
165
+ end
166
+ end
167
+
168
+ nil
169
+ end
170
+
171
+ # Extract custom actions from resource class
172
+ def extract_custom_actions(resource_class)
173
+ custom_actions = []
174
+
175
+ # Look for custom controller actions by examining routes or method definitions
176
+ # This is a simplified approach - could be expanded based on EasyAdmin's routing
177
+
178
+ # Check if resource has custom action definitions
179
+ if resource_class.respond_to?(:custom_actions)
180
+ custom_actions.concat(resource_class.custom_actions)
181
+ end
182
+
183
+ # Look for batch actions
184
+ if resource_class.respond_to?(:batch_actions) && resource_class.batch_actions.any?
185
+ custom_actions << "batch_actions"
186
+ end
187
+
188
+ custom_actions.uniq
189
+ end
190
+
191
+ # Generate human-readable description for permission
192
+ def generate_permission_description(resource_name, action)
193
+ resource_humanized = resource_name.humanize.downcase
194
+
195
+ case action
196
+ when 'index'
197
+ "View list of #{resource_humanized.pluralize}"
198
+ when 'show', 'read'
199
+ "View #{resource_humanized} details"
200
+ when 'new'
201
+ "Access new #{resource_humanized} form"
202
+ when 'create'
203
+ "Create new #{resource_humanized.pluralize}"
204
+ when 'edit'
205
+ "Access #{resource_humanized} edit form"
206
+ when 'update'
207
+ "Update existing #{resource_humanized.pluralize}"
208
+ when 'destroy', 'delete'
209
+ "Delete #{resource_humanized.pluralize}"
210
+ when 'export'
211
+ "Export #{resource_humanized} data"
212
+ when 'import'
213
+ "Import #{resource_humanized} data"
214
+ when 'batch_update'
215
+ "Batch update multiple #{resource_humanized.pluralize}"
216
+ when 'batch_delete'
217
+ "Batch delete multiple #{resource_humanized.pluralize}"
218
+ when 'batch_actions'
219
+ "Perform batch actions on #{resource_humanized.pluralize}"
220
+ when 'row_actions'
221
+ "Execute row actions on #{resource_humanized.pluralize}"
222
+ when 'manage_versions'
223
+ "Manage version history for #{resource_humanized.pluralize}"
224
+ else
225
+ "#{action.humanize} #{resource_humanized.pluralize}"
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,45 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class RoleDefinition
4
+ attr_reader :name, :slug, :description, :permissions
5
+
6
+ def initialize(name, slug: nil, description: nil)
7
+ @name = name
8
+ @slug = slug || name.parameterize
9
+ @description = description
10
+ @permissions = {}
11
+ end
12
+
13
+ # Grant permissions for a resource
14
+ def can(actions, resource)
15
+ Array(actions).each do |action|
16
+ permission_key = "#{resource}:#{action}"
17
+ @permissions[permission_key] = true
18
+ end
19
+ end
20
+
21
+ # Deny permissions for a resource (explicit)
22
+ def cannot(actions, resource)
23
+ Array(actions).each do |action|
24
+ permission_key = "#{resource}:#{action}"
25
+ @permissions[permission_key] = false
26
+ end
27
+ end
28
+
29
+ # Grant all CRUD permissions for a resource
30
+ def manage(resource)
31
+ can([:read, :create, :update, :delete], resource)
32
+ end
33
+
34
+ # Check if role has a specific permission
35
+ def has_permission?(permission_key)
36
+ @permissions[permission_key] == true
37
+ end
38
+
39
+ # Get all granted permissions
40
+ def granted_permissions
41
+ @permissions.select { |_, granted| granted == true }.keys
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,159 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class RoleDeniedComponent < EasyAdmin::BaseComponent
4
+ def initialize(role:, user: nil, context: nil)
5
+ @role = role
6
+ @user = user
7
+ @context = context
8
+ end
9
+
10
+ def view_template
11
+ div(class: "min-h-96 flex items-center justify-center p-8") do
12
+ div(class: "text-center max-w-md mx-auto") do
13
+ # Icon
14
+ div(class: "mb-6") do
15
+ svg(class: "w-20 h-20 mx-auto text-amber-400", fill: "currentColor", viewBox: "0 0 24 24") do
16
+ path(d: "M5 16L3 14L5 12L3 10L5 8L11 14L5 16ZM19 8L21 10L19 12L21 14L19 16L13 10L19 8ZM12 2L15.09 8.26L22 9L17 14L18.18 21L12 17.77L5.82 21L7 14L2 9L8.91 8.26L12 2Z")
17
+ end
18
+ end
19
+
20
+ # Title
21
+ h2(class: "text-3xl font-bold text-gray-900 mb-4") { "Role Required" }
22
+
23
+ # Description
24
+ div(class: "text-gray-600 mb-6 space-y-2") do
25
+ p { "You need a specific role to access this resource." }
26
+ if @role
27
+ div(class: "text-sm bg-amber-50 px-3 py-2 rounded-lg border border-amber-200") do
28
+ span(class: "text-gray-500") { "Required role: " }
29
+ span(class: "text-amber-700 font-semibold") { @role.to_s.humanize }
30
+ end
31
+ end
32
+ if @context
33
+ div(class: "text-sm text-gray-500") do
34
+ span { "Context: " }
35
+ span(class: "font-medium") { @context.to_s }
36
+ end
37
+ end
38
+ end
39
+
40
+ # User info (if available)
41
+ if @user
42
+ div(class: "text-sm text-gray-600 mb-6 p-3 bg-gray-50 rounded-lg") do
43
+ p do
44
+ span { "Signed in as: " }
45
+ span(class: "font-medium text-gray-700") { @user.email || @user.name || "User ##{@user.id}" }
46
+ end
47
+
48
+ # Show current roles
49
+ if @user.respond_to?(:roles)
50
+ current_roles = @user.roles.active
51
+ if current_roles.any?
52
+ p(class: "mt-2") do
53
+ span { "Your current roles: " }
54
+ current_roles.each do |role|
55
+ span(class: "inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800 mr-1") do
56
+ role.name
57
+ end
58
+ end
59
+ end
60
+ else
61
+ p(class: "mt-2 text-gray-500 italic") { "No roles assigned" }
62
+ end
63
+ end
64
+
65
+ # Show what the required role would give access to
66
+ if show_role_benefits?
67
+ div(class: "mt-3 pt-3 border-t border-gray-200") do
68
+ p(class: "text-xs text-gray-500 mb-2") { "The #{@role.to_s.humanize} role includes:" }
69
+ div(class: "text-xs text-gray-600 space-y-1") do
70
+ role_permissions.each do |permission|
71
+ div(class: "flex items-center") do
72
+ span(class: "text-green-500 mr-1") { "✓" }
73
+ span { permission.humanize }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Actions
83
+ div(class: "space-y-3") do
84
+ # Go back button
85
+ button(
86
+ onclick: "history.back()",
87
+ class: "w-full px-6 py-3 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors font-medium"
88
+ ) do
89
+ "← Go Back"
90
+ end
91
+
92
+ # Request role access (if configured)
93
+ if role_request_available?
94
+ a(
95
+ href: role_request_url,
96
+ class: "inline-block text-sm text-amber-600 hover:text-amber-800 transition-colors"
97
+ ) do
98
+ "Request #{@role.to_s.humanize} role access"
99
+ end
100
+ end
101
+ end
102
+
103
+ # Additional info
104
+ div(class: "mt-8 pt-6 border-t border-gray-200 text-xs text-gray-400") do
105
+ p { "Contact your administrator to request the required role." }
106
+ if Rails.env.development?
107
+ div(class: "mt-2 p-2 bg-yellow-50 rounded text-yellow-700 text-left font-mono text-xs") do
108
+ p { "Dev info:" }
109
+ p { "Required role: #{@role}" }
110
+ p { "Context: #{@context}" } if @context
111
+ p { "User ID: #{@user&.id}" }
112
+ p { "Current roles: #{@user&.roles&.pluck(:name)&.join(', ')}" } if @user&.respond_to?(:roles)
113
+ p { "Time: #{Time.current}" }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def show_role_benefits?
124
+ role_permissions.any? && Rails.env.development?
125
+ end
126
+
127
+ def role_permissions
128
+ return [] unless defined?(EasyAdmin::Permissions::Role)
129
+
130
+ @role_permissions ||= begin
131
+ role_record = EasyAdmin::Permissions::Role.find_by(slug: @role.to_s) ||
132
+ EasyAdmin::Permissions::Role.find_by(name: @role.to_s.humanize)
133
+ role_record&.permissions&.limit(5)&.pluck(:description) || []
134
+ end
135
+ end
136
+
137
+ def role_request_available?
138
+ respond_to?(:admin_email) ||
139
+ defined?(Rails.application.config.admin_email) ||
140
+ defined?(EasyAdmin.configuration&.support_email)
141
+ end
142
+
143
+ def role_request_url
144
+ email_subject = "Role%20Access%20Request%20-%20#{@role.to_s.humanize}"
145
+ email_body = "Hello,%0D%0A%0D%0AI%20would%20like%20to%20request%20access%20to%20the%20#{@role.to_s.humanize}%20role.%0D%0A%0D%0AThank%20you"
146
+
147
+ if respond_to?(:admin_email)
148
+ "mailto:#{admin_email}?subject=#{email_subject}&body=#{email_body}"
149
+ elsif defined?(Rails.application.config.admin_email)
150
+ "mailto:#{Rails.application.config.admin_email}?subject=#{email_subject}&body=#{email_body}"
151
+ elsif defined?(EasyAdmin.configuration&.support_email)
152
+ "mailto:#{EasyAdmin.configuration.support_email}?subject=#{email_subject}&body=#{email_body}"
153
+ else
154
+ "#"
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,73 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class RoleDSL
4
+ attr_reader :roles
5
+
6
+ def initialize
7
+ @roles = {}
8
+ end
9
+
10
+ # Define a role with permissions
11
+ def role(name, slug: nil, description: nil, &block)
12
+ role_def = RoleDefinition.new(name, slug: slug, description: description)
13
+
14
+ if block_given?
15
+ RolePermissionDSL.new(role_def).instance_eval(&block)
16
+ end
17
+
18
+ @roles[role_def.slug] = role_def
19
+ role_def
20
+ end
21
+
22
+ # Get role by slug
23
+ def get_role(slug)
24
+ @roles[slug.to_s]
25
+ end
26
+
27
+ # Get all role slugs
28
+ def role_slugs
29
+ @roles.keys
30
+ end
31
+
32
+ # Get all roles
33
+ def all_roles
34
+ @roles.values
35
+ end
36
+ end
37
+
38
+ class RolePermissionDSL
39
+ def initialize(role_definition)
40
+ @role = role_definition
41
+ end
42
+
43
+ # Grant permissions
44
+ def can(actions, resource)
45
+ @role.can(actions, resource)
46
+ end
47
+
48
+ # Deny permissions
49
+ def cannot(actions, resource)
50
+ @role.cannot(actions, resource)
51
+ end
52
+
53
+ # Grant all CRUD permissions
54
+ def manage(resource)
55
+ @role.manage(resource)
56
+ end
57
+
58
+ # Grant permissions for all registered EasyAdmin resources
59
+ def manage_all_resources
60
+ EasyAdmin::Permissions.available_resources.each do |resource|
61
+ manage(resource)
62
+ end
63
+ end
64
+
65
+ # Grant read-only access to all resources
66
+ def read_all_resources
67
+ EasyAdmin::Permissions.available_resources.each do |resource|
68
+ can(:read, resource)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end