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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +254 -18
- data/app/assets/builds/easy_admin.base.js.map +4 -4
- data/app/assets/builds/easy_admin.css +112 -18
- data/app/components/easy_admin/base_component.rb +1 -0
- data/app/components/easy_admin/form_tabs_component.rb +5 -2
- data/app/components/easy_admin/navbar_component.rb +5 -1
- data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
- data/app/components/easy_admin/resources/index_component.rb +1 -4
- data/app/components/easy_admin/sidebar_component.rb +67 -2
- data/app/components/easy_admin/versions/diff_modal_component.rb +5 -1
- data/app/controllers/easy_admin/application_controller.rb +131 -1
- data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
- data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
- data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
- data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
- data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
- data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
- data/app/controllers/easy_admin/resources_controller.rb +13 -762
- data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
- data/app/helpers/easy_admin/fields_helper.rb +61 -9
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
- data/app/javascript/easy_admin/controllers.js +5 -1
- data/app/models/easy_admin/admin_user.rb +6 -0
- data/app/policies/admin_user_policy.rb +36 -0
- data/app/policies/application_policy.rb +83 -0
- data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
- data/app/views/easy_admin/dashboards/card.html.erb +5 -0
- data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
- data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
- data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
- data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
- data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
- data/app/views/easy_admin/resources/edit.html.erb +1 -1
- data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
- data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/index.html.erb +1 -1
- data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
- data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
- data/app/views/layouts/easy_admin/application.html.erb +15 -2
- data/config/initializers/easy_admin_permissions.rb +73 -0
- data/db/seeds/easy_admin_permissions.rb +121 -0
- data/lib/easy-admin-rails.rb +2 -0
- data/lib/easy_admin/permissions/component.rb +168 -0
- data/lib/easy_admin/permissions/configuration.rb +37 -0
- data/lib/easy_admin/permissions/controller.rb +164 -0
- data/lib/easy_admin/permissions/dsl.rb +180 -0
- data/lib/easy_admin/permissions/models.rb +44 -0
- data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
- data/lib/easy_admin/permissions/role_definition.rb +45 -0
- data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
- data/lib/easy_admin/permissions/role_dsl.rb +73 -0
- data/lib/easy_admin/permissions/user_extensions.rb +129 -0
- data/lib/easy_admin/permissions.rb +113 -0
- data/lib/easy_admin/resource/base.rb +119 -0
- data/lib/easy_admin/resource/configuration.rb +148 -0
- data/lib/easy_admin/resource/dsl.rb +117 -0
- data/lib/easy_admin/resource/field_registry.rb +189 -0
- data/lib/easy_admin/resource/form_builder.rb +123 -0
- data/lib/easy_admin/resource/layout_builder.rb +249 -0
- data/lib/easy_admin/resource/scope_manager.rb +252 -0
- data/lib/easy_admin/resource/show_builder.rb +359 -0
- data/lib/easy_admin/resource.rb +8 -835
- data/lib/easy_admin/resource_modules.rb +11 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
- data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
- data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
- data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
- data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
- metadata +62 -5
- 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
|