easy-admin-rails 0.1.15 → 0.2.1

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 (92) 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/controllers/easy_admin/application_controller.rb +131 -1
  13. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  14. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  15. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  16. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  17. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  18. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  19. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  20. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  21. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  22. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  23. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  24. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  25. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  26. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  27. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  28. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  29. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  30. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  31. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  33. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  34. data/app/javascript/easy_admin/controllers.js +5 -1
  35. data/app/models/easy_admin/admin_user.rb +6 -0
  36. data/app/policies/admin_user_policy.rb +36 -0
  37. data/app/policies/application_policy.rb +83 -0
  38. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  39. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  40. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  41. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  42. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  43. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  44. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  46. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  47. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  48. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  49. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  50. data/app/views/easy_admin/resources/index.html.erb +1 -1
  51. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  52. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  53. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  54. data/config/initializers/easy_admin_permissions.rb +73 -0
  55. data/db/seeds/easy_admin_permissions.rb +121 -0
  56. data/lib/easy-admin-rails.rb +2 -0
  57. data/lib/easy_admin/permissions/component.rb +168 -0
  58. data/lib/easy_admin/permissions/configuration.rb +37 -0
  59. data/lib/easy_admin/permissions/controller.rb +164 -0
  60. data/lib/easy_admin/permissions/dsl.rb +160 -0
  61. data/lib/easy_admin/permissions/models.rb +44 -0
  62. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  63. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  64. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  65. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  66. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  67. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  68. data/lib/easy_admin/permissions.rb +113 -0
  69. data/lib/easy_admin/resource/base.rb +119 -0
  70. data/lib/easy_admin/resource/configuration.rb +148 -0
  71. data/lib/easy_admin/resource/dsl.rb +117 -0
  72. data/lib/easy_admin/resource/field_registry.rb +189 -0
  73. data/lib/easy_admin/resource/form_builder.rb +123 -0
  74. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  75. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  76. data/lib/easy_admin/resource/show_builder.rb +359 -0
  77. data/lib/easy_admin/resource.rb +8 -835
  78. data/lib/easy_admin/resource_modules.rb +11 -0
  79. data/lib/easy_admin/version.rb +1 -1
  80. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  81. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  82. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  84. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  90. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  91. metadata +62 -5
  92. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,168 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ module Component
4
+ extend ActiveSupport::Concern
5
+
6
+ # Check if current user has permission
7
+ def current_user_can?(permission_name, context: nil)
8
+ current_user = helpers.current_user if helpers.respond_to?(:current_user)
9
+ EasyAdmin::Permissions.authorized?(current_user, permission_name, context: context)
10
+ end
11
+
12
+ # Check if current user has role
13
+ def current_user_has_role?(role_name, context: nil)
14
+ current_user = helpers.current_user if helpers.respond_to?(:current_user)
15
+ EasyAdmin::Permissions.has_role?(current_user, role_name, context: context)
16
+ end
17
+
18
+ # Render content only if user has permission
19
+ def if_can(permission_name, context: nil, &block)
20
+ if current_user_can?(permission_name, context: context)
21
+ block.call if block_given?
22
+ end
23
+ end
24
+
25
+ # Render content only if user has role
26
+ def if_has_role(role_name, context: nil, &block)
27
+ if current_user_has_role?(role_name, context: context)
28
+ block.call if block_given?
29
+ end
30
+ end
31
+
32
+ # Render content if user DOESN'T have permission
33
+ def unless_can(permission_name, context: nil, &block)
34
+ unless current_user_can?(permission_name, context: context)
35
+ block.call if block_given?
36
+ end
37
+ end
38
+
39
+ # Render content if user DOESN'T have role
40
+ def unless_has_role(role_name, context: nil, &block)
41
+ unless current_user_has_role?(role_name, context: context)
42
+ block.call if block_given?
43
+ end
44
+ end
45
+
46
+ # Conditional CSS classes based on permissions
47
+ def permission_classes(permission_name, enabled_classes: "", disabled_classes: "opacity-50 cursor-not-allowed", context: nil)
48
+ if current_user_can?(permission_name, context: context)
49
+ enabled_classes
50
+ else
51
+ disabled_classes
52
+ end
53
+ end
54
+
55
+ # Conditional attributes based on permissions
56
+ def permission_attrs(permission_name, enabled_attrs: {}, disabled_attrs: {}, context: nil)
57
+ if current_user_can?(permission_name, context: context)
58
+ enabled_attrs
59
+ else
60
+ disabled_attrs
61
+ end
62
+ end
63
+
64
+ # Generate link with permission check
65
+ def permission_link(text, href, permission_name, context: nil, **attrs, &block)
66
+ if current_user_can?(permission_name, context: context)
67
+ a(href: href, **attrs) do
68
+ if block_given?
69
+ block.call
70
+ else
71
+ text
72
+ end
73
+ end
74
+ else
75
+ span(class: "text-gray-400 cursor-not-allowed", **attrs.except(:href, :data)) do
76
+ if block_given?
77
+ block.call
78
+ else
79
+ text
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ # Generate button with permission check
86
+ def permission_button(text = nil, permission_name:, context: nil, disabled_class: "opacity-50 cursor-not-allowed", **attrs, &block)
87
+ can_access = current_user_can?(permission_name, context: context)
88
+
89
+ button_attrs = attrs.dup
90
+ button_attrs[:disabled] = true unless can_access
91
+ button_attrs[:class] = [button_attrs[:class], disabled_class].compact.join(" ") unless can_access
92
+
93
+ button(**button_attrs) do
94
+ if block_given?
95
+ block.call
96
+ elsif text
97
+ text
98
+ end
99
+ end
100
+ end
101
+
102
+ # Render form field only if user can edit
103
+ def permission_field(permission_name, context: nil, readonly_class: "bg-gray-100", &block)
104
+ can_edit = current_user_can?(permission_name, context: context)
105
+
106
+ if can_edit
107
+ block.call if block_given?
108
+ else
109
+ # Render read-only version
110
+ div(class: readonly_class) do
111
+ block.call if block_given?
112
+ end
113
+ end
114
+ end
115
+
116
+ # Show different content based on multiple permission checks
117
+ def permission_case(context: nil, &block)
118
+ permission_case_builder = PermissionCaseBuilder.new(self, context)
119
+ permission_case_builder.instance_eval(&block) if block_given?
120
+ permission_case_builder.render
121
+ end
122
+
123
+ private
124
+
125
+ # Helper class for building conditional permission rendering
126
+ class PermissionCaseBuilder
127
+ def initialize(component, context)
128
+ @component = component
129
+ @context = context
130
+ @cases = []
131
+ @else_block = nil
132
+ end
133
+
134
+ def when_can(permission_name, &block)
135
+ @cases << { type: :permission, name: permission_name, block: block }
136
+ self
137
+ end
138
+
139
+ def when_has_role(role_name, &block)
140
+ @cases << { type: :role, name: role_name, block: block }
141
+ self
142
+ end
143
+
144
+ def otherwise(&block)
145
+ @else_block = block
146
+ self
147
+ end
148
+
149
+ def render
150
+ @cases.each do |case_item|
151
+ case case_item[:type]
152
+ when :permission
153
+ if @component.current_user_can?(case_item[:name], context: @context)
154
+ return case_item[:block].call if case_item[:block]
155
+ end
156
+ when :role
157
+ if @component.current_user_has_role?(case_item[:name], context: @context)
158
+ return case_item[:block].call if case_item[:block]
159
+ end
160
+ end
161
+ end
162
+
163
+ @else_block&.call
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,37 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class Configuration
4
+ attr_accessor :enabled,
5
+ :cache_duration,
6
+ :admin_bypass,
7
+ :user_model,
8
+ :context_types
9
+
10
+ def initialize
11
+ @enabled = false
12
+ @cache_duration = 1.hour
13
+ @admin_bypass = true
14
+ @user_model = 'User'
15
+ @context_types = [] # e.g., ['Organization', 'Project']
16
+ end
17
+
18
+ # Define available permission contexts
19
+ def contexts(*types)
20
+ @context_types = types.map(&:to_s)
21
+ end
22
+
23
+ # Set the user model class
24
+ def user_class(klass = nil)
25
+ if klass
26
+ @user_model = klass.to_s
27
+ else
28
+ @user_model.constantize
29
+ end
30
+ end
31
+
32
+ def user_class_name
33
+ @user_model
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,164 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ module Controller
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # Include Action Policy authorization
8
+ include ActionPolicy::Controller if defined?(ActionPolicy)
9
+
10
+ # Set up authorization context for Action Policy
11
+ if respond_to?(:authorize)
12
+ authorize :user, through: :current_user
13
+ end
14
+ end
15
+
16
+ # Check if current user has permission
17
+ def current_user_can?(permission_name, context: nil)
18
+ EasyAdmin::Permissions.authorized?(current_user, permission_name, context: context)
19
+ end
20
+
21
+ # Check if current user has role
22
+ def current_user_has_role?(role_name, context: nil)
23
+ EasyAdmin::Permissions.has_role?(current_user, role_name, context: context)
24
+ end
25
+
26
+ # Require permission or show 403 error
27
+ def require_permission!(permission_name, context: nil)
28
+ unless current_user_can?(permission_name, context: context)
29
+ handle_permission_denied(permission_name)
30
+ end
31
+ end
32
+
33
+ # Require role or show 403 error
34
+ def require_role!(role_name, context: nil)
35
+ unless current_user_has_role?(role_name, context: context)
36
+ handle_role_denied(role_name)
37
+ end
38
+ end
39
+
40
+ # Before action to check permissions for CRUD operations
41
+ def check_permissions_for_action
42
+ action = action_name.to_s
43
+ resource_name = controller_name
44
+
45
+ permission_map = {
46
+ 'index' => "#{resource_name}:read",
47
+ 'show' => "#{resource_name}:read",
48
+ 'new' => "#{resource_name}:create",
49
+ 'create' => "#{resource_name}:create",
50
+ 'edit' => "#{resource_name}:update",
51
+ 'update' => "#{resource_name}:update",
52
+ 'destroy' => "#{resource_name}:delete"
53
+ }
54
+
55
+ if permission_name = permission_map[action]
56
+ require_permission!(permission_name)
57
+ end
58
+ end
59
+
60
+ # Get current user's permissions for view helpers
61
+ def current_user_permissions(context: nil)
62
+ return [] unless current_user
63
+ EasyAdmin::Permissions.user_permissions(current_user, context: context)
64
+ end
65
+
66
+ # Check permission in views (helper method)
67
+ def can?(permission_name, context: nil)
68
+ current_user_can?(permission_name, context: context)
69
+ end
70
+
71
+ # Check role in views (helper method)
72
+ def has_role?(role_name, context: nil)
73
+ current_user_has_role?(role_name, context: context)
74
+ end
75
+
76
+ private
77
+
78
+ def handle_permission_denied(permission_name)
79
+ respond_to do |format|
80
+ format.html do
81
+ if request.xhr? || request.headers['Content-Type'] == 'text/html'
82
+ render plain: permission_denied_component(permission_name).call, status: :forbidden
83
+ else
84
+ redirect_to root_path, alert: "Permission denied: #{permission_name}"
85
+ end
86
+ end
87
+ format.json { render json: { error: "Permission denied: #{permission_name}" }, status: :forbidden }
88
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("main", permission_denied_component(permission_name).call) }
89
+ end
90
+ end
91
+
92
+ def handle_role_denied(role_name)
93
+ respond_to do |format|
94
+ format.html do
95
+ if request.xhr? || request.headers['Content-Type'] == 'text/html'
96
+ render plain: role_denied_component(role_name).call, status: :forbidden
97
+ else
98
+ redirect_to root_path, alert: "Role required: #{role_name}"
99
+ end
100
+ end
101
+ format.json { render json: { error: "Role required: #{role_name}" }, status: :forbidden }
102
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("main", role_denied_component(role_name).call) }
103
+ end
104
+ end
105
+
106
+ def permission_denied_component(permission_name)
107
+ if defined?(EasyAdmin::Permissions::PermissionDeniedComponent)
108
+ EasyAdmin::Permissions::PermissionDeniedComponent.new(permission: permission_name, user: current_user)
109
+ else
110
+ # Fallback component
111
+ BasicPermissionDeniedComponent.new(permission: permission_name)
112
+ end
113
+ end
114
+
115
+ def role_denied_component(role_name)
116
+ if defined?(EasyAdmin::Permissions::RoleDeniedComponent)
117
+ EasyAdmin::Permissions::RoleDeniedComponent.new(role: role_name, user: current_user)
118
+ else
119
+ # Fallback component
120
+ BasicRoleDeniedComponent.new(role: role_name)
121
+ end
122
+ end
123
+
124
+ # Basic fallback components for when full components aren't defined
125
+ class BasicPermissionDeniedComponent
126
+ def initialize(permission:)
127
+ @permission = permission
128
+ end
129
+
130
+ def call
131
+ <<~HTML
132
+ <div class="flex items-center justify-center min-h-96">
133
+ <div class="text-center">
134
+ <div class="text-6xl text-gray-400 mb-4">🔒</div>
135
+ <h2 class="text-2xl font-bold text-gray-900 mb-2">Permission Denied</h2>
136
+ <p class="text-gray-600">You need the '#{@permission}' permission to access this resource.</p>
137
+ <a href="javascript:history.back()" class="inline-block mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Go Back</a>
138
+ </div>
139
+ </div>
140
+ HTML
141
+ end
142
+ end
143
+
144
+ class BasicRoleDeniedComponent
145
+ def initialize(role:)
146
+ @role = role
147
+ end
148
+
149
+ def call
150
+ <<~HTML
151
+ <div class="flex items-center justify-center min-h-96">
152
+ <div class="text-center">
153
+ <div class="text-6xl text-gray-400 mb-4">👑</div>
154
+ <h2 class="text-2xl font-bold text-gray-900 mb-2">Role Required</h2>
155
+ <p class="text-gray-600">You need the '#{@role}' role to access this resource.</p>
156
+ <a href="javascript:history.back()" class="inline-block mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Go Back</a>
157
+ </div>
158
+ </div>
159
+ HTML
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,160 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class DSL
4
+ def initialize
5
+ @permissions = []
6
+ @roles = []
7
+ end
8
+
9
+ # Define a permission
10
+ def permission(resource_type, action, description: nil, conditions: {})
11
+ name = "#{resource_type}:#{action}"
12
+
13
+ @permissions << {
14
+ name: name,
15
+ resource_type: resource_type.to_s,
16
+ action: action.to_s,
17
+ description: description || "#{action.to_s.humanize} #{resource_type.to_s.humanize}",
18
+ conditions: conditions
19
+ }
20
+ end
21
+
22
+ # Define a role with permissions
23
+ def role(name, slug: nil, description: nil, permissions: [], active: true, metadata: {})
24
+ @roles << {
25
+ name: name.to_s.humanize,
26
+ slug: slug&.to_s || name.to_s.parameterize,
27
+ description: description,
28
+ permissions: permissions.map(&:to_s),
29
+ active: active,
30
+ metadata: metadata
31
+ }
32
+ end
33
+
34
+ # Shortcut methods for common permission patterns
35
+ def crud_permissions(resource_type, description_prefix: nil)
36
+ prefix = description_prefix || resource_type.to_s.humanize
37
+
38
+ permission resource_type, :read, description: "View #{prefix.downcase}"
39
+ permission resource_type, :create, description: "Create new #{prefix.downcase.singularize}"
40
+ permission resource_type, :update, description: "Edit existing #{prefix.downcase}"
41
+ permission resource_type, :delete, description: "Delete #{prefix.downcase}"
42
+ end
43
+
44
+ def admin_permissions(resource_type, description_prefix: nil)
45
+ crud_permissions(resource_type, description_prefix: description_prefix)
46
+ permission resource_type, :manage, description: "Full management of #{(description_prefix || resource_type.to_s.humanize).downcase}"
47
+ end
48
+
49
+ # Resource shortcuts
50
+ def resource(resource_type, actions: [:read, :create, :update, :delete], description_prefix: nil)
51
+ prefix = description_prefix || resource_type.to_s.humanize
52
+
53
+ actions.each do |action|
54
+ case action
55
+ when :read
56
+ permission resource_type, :read, description: "View #{prefix.downcase}"
57
+ when :create
58
+ permission resource_type, :create, description: "Create new #{prefix.downcase.singularize}"
59
+ when :update
60
+ permission resource_type, :update, description: "Edit existing #{prefix.downcase}"
61
+ when :delete
62
+ permission resource_type, :delete, description: "Delete #{prefix.downcase}"
63
+ else
64
+ permission resource_type, action, description: "#{action.to_s.humanize} #{prefix.downcase}"
65
+ end
66
+ end
67
+ end
68
+
69
+ # EasyAdmin resource shortcuts using actual resource discovery
70
+ def easy_admin_resource(resource_name, actions: nil)
71
+ available_actions = EasyAdmin::Permissions::ResourcePermissions.actions_for_resource(resource_name)
72
+ actions_to_create = actions || available_actions
73
+
74
+ actions_to_create.each do |action|
75
+ next unless available_actions.include?(action.to_s)
76
+
77
+ permission_data = EasyAdmin::Permissions::ResourcePermissions.discover_all_permissions
78
+ .find { |p| p[:resource_type] == resource_name.to_s && p[:action] == action.to_s }
79
+
80
+ if permission_data
81
+ permission resource_name, action, description: permission_data[:description]
82
+ else
83
+ permission resource_name, action
84
+ end
85
+ end
86
+ end
87
+
88
+ # Auto-discover and create permissions for all EasyAdmin resources
89
+ def auto_discover_resources(actions: nil)
90
+ EasyAdmin::Permissions::ResourcePermissions.available_resources.each do |resource_name|
91
+ easy_admin_resource(resource_name, actions: actions)
92
+ end
93
+ end
94
+
95
+ # Enhanced role definition with EasyAdmin resource support
96
+ def easy_admin_role(name, slug: nil, description: nil, resources: {}, permissions: [], active: true, metadata: {})
97
+ # Convert resources hash to permission names
98
+ resource_permissions = []
99
+
100
+ resources.each do |resource_name, resource_actions|
101
+ Array(resource_actions).each do |action|
102
+ resource_permissions << "#{resource_name}:#{action}"
103
+ end
104
+ end
105
+
106
+ # Combine with manual permissions
107
+ all_permissions = resource_permissions + permissions.map(&:to_s)
108
+
109
+ role(name, slug: slug, description: description, permissions: all_permissions, active: active, metadata: metadata)
110
+ end
111
+
112
+ # Execute the DSL block and return the collected data
113
+ def self.evaluate(&block)
114
+ dsl = new
115
+ dsl.instance_eval(&block) if block_given?
116
+ {
117
+ permissions: dsl.instance_variable_get(:@permissions),
118
+ roles: dsl.instance_variable_get(:@roles)
119
+ }
120
+ end
121
+
122
+ # Create roles in the database (simplified - no permissions table)
123
+ def self.seed_database(data)
124
+ # Skip if tables don't exist yet (during migrations or first setup)
125
+ return unless ActiveRecord::Base.connection.table_exists?(:easy_admin_roles)
126
+
127
+ # Defer execution until Rails is fully initialized
128
+ if defined?(Rails) && Rails.application && !Rails.application.initialized?
129
+ Rails.application.config.after_initialize do
130
+ # Check if models are available (avoid loading during initialization)
131
+ begin
132
+ role_class = "EasyAdmin::Permissions::Role".constantize
133
+ rescue NameError => e
134
+ Rails.logger.debug "EasyAdmin::Permissions models not yet loaded: #{e.message}"
135
+ return
136
+ end
137
+
138
+ ActiveRecord::Base.transaction do
139
+ # Create roles only (permissions are managed via JSON field)
140
+ data[:roles].each do |role_data|
141
+ role = role_class.find_or_initialize_by(slug: role_data[:slug])
142
+ role.assign_attributes(
143
+ name: role_data[:name],
144
+ description: role_data[:description],
145
+ )
146
+ role.save! if role.changed?
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ # Main method to define permissions and roles
155
+ def self.define(&block)
156
+ data = DSL.evaluate(&block)
157
+ DSL.seed_database(data)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,44 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ module Models
4
+ extend ActiveSupport::Concern
5
+
6
+ # Include this in your Role model
7
+ module Role
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ has_many :user_roles, class_name: 'EasyAdmin::Permissions::UserRole', dependent: :destroy
12
+ has_many :users, through: :user_roles, class_name: EasyAdmin::Permissions.configuration.user_class_name
13
+
14
+ validates :name, presence: true, uniqueness: true
15
+ validates :slug, presence: true, uniqueness: true
16
+
17
+ before_validation :generate_slug, if: -> { slug.blank? }
18
+
19
+ scope :active, -> { where(active: true) }
20
+ end
21
+
22
+ private
23
+
24
+ def generate_slug
25
+ self.slug = name&.parameterize
26
+ end
27
+ end
28
+
29
+ # Include this in your UserRole model (join table)
30
+ module UserRole
31
+ extend ActiveSupport::Concern
32
+
33
+ included do
34
+ belongs_to :user, class_name: EasyAdmin::Permissions.configuration.user_class_name, foreign_key: 'user_id'
35
+ belongs_to :role, class_name: 'EasyAdmin::Permissions::Role'
36
+
37
+ validates :user_id, uniqueness: { scope: :role_id }
38
+
39
+ scope :active, -> { where(active: true) }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -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