cccux 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +382 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/config/cccux_manifest.js +1 -0
  7. data/app/assets/stylesheets/cccux/application.css +102 -0
  8. data/app/controllers/cccux/ability_permissions_controller.rb +271 -0
  9. data/app/controllers/cccux/application_controller.rb +37 -0
  10. data/app/controllers/cccux/authorization_controller.rb +10 -0
  11. data/app/controllers/cccux/cccux_controller.rb +64 -0
  12. data/app/controllers/cccux/dashboard_controller.rb +172 -0
  13. data/app/controllers/cccux/home_controller.rb +19 -0
  14. data/app/controllers/cccux/roles_controller.rb +290 -0
  15. data/app/controllers/cccux/simple_controller.rb +7 -0
  16. data/app/controllers/cccux/users_controller.rb +112 -0
  17. data/app/controllers/concerns/cccux/application_controller_concern.rb +32 -0
  18. data/app/helpers/cccux/application_helper.rb +4 -0
  19. data/app/helpers/cccux/authorization_helper.rb +228 -0
  20. data/app/jobs/cccux/application_job.rb +4 -0
  21. data/app/mailers/cccux/application_mailer.rb +6 -0
  22. data/app/models/cccux/ability.rb +142 -0
  23. data/app/models/cccux/ability_permission.rb +61 -0
  24. data/app/models/cccux/application_record.rb +5 -0
  25. data/app/models/cccux/role.rb +90 -0
  26. data/app/models/cccux/role_ability.rb +49 -0
  27. data/app/models/cccux/user_role.rb +42 -0
  28. data/app/models/concerns/cccux/authorizable.rb +25 -0
  29. data/app/models/concerns/cccux/scoped_ownership.rb +183 -0
  30. data/app/models/concerns/cccux/user_concern.rb +87 -0
  31. data/app/views/cccux/ability_permissions/edit.html.erb +58 -0
  32. data/app/views/cccux/ability_permissions/index.html.erb +108 -0
  33. data/app/views/cccux/ability_permissions/new.html.erb +308 -0
  34. data/app/views/cccux/dashboard/index.html.erb +69 -0
  35. data/app/views/cccux/dashboard/model_discovery.html.erb +148 -0
  36. data/app/views/cccux/home/index.html.erb +42 -0
  37. data/app/views/cccux/roles/_flash.html.erb +10 -0
  38. data/app/views/cccux/roles/_form.html.erb +78 -0
  39. data/app/views/cccux/roles/_role.html.erb +67 -0
  40. data/app/views/cccux/roles/edit.html.erb +317 -0
  41. data/app/views/cccux/roles/index.html.erb +51 -0
  42. data/app/views/cccux/roles/new.html.erb +3 -0
  43. data/app/views/cccux/roles/show.html.erb +99 -0
  44. data/app/views/cccux/users/edit.html.erb +117 -0
  45. data/app/views/cccux/users/index.html.erb +99 -0
  46. data/app/views/cccux/users/new.html.erb +94 -0
  47. data/app/views/cccux/users/show.html.erb +138 -0
  48. data/app/views/layouts/cccux/admin.html.erb +168 -0
  49. data/app/views/layouts/cccux/application.html.erb +17 -0
  50. data/app/views/shared/_footer.html.erb +101 -0
  51. data/config/routes.rb +63 -0
  52. data/db/migrate/20250626194001_create_cccux_roles.rb +15 -0
  53. data/db/migrate/20250626194007_create_cccux_ability_permissions.rb +18 -0
  54. data/db/migrate/20250626194011_create_cccux_user_roles.rb +13 -0
  55. data/db/migrate/20250626194016_create_cccux_role_abilities.rb +10 -0
  56. data/db/migrate/20250627170611_add_owned_to_cccux_role_abilities.rb +9 -0
  57. data/db/migrate/20250705193709_add_context_to_cccux_role_abilities.rb +9 -0
  58. data/db/migrate/20250706214415_add_ownership_configuration_to_role_abilities.rb +21 -0
  59. data/db/seeds.rb +136 -0
  60. data/lib/cccux/engine.rb +50 -0
  61. data/lib/cccux/version.rb +3 -0
  62. data/lib/cccux.rb +7 -0
  63. data/lib/tasks/cccux.rake +703 -0
  64. data/lib/tasks/view_helpers.rake +274 -0
  65. metadata +188 -0
@@ -0,0 +1,290 @@
1
+ module Cccux
2
+ class RolesController < CccuxController
3
+ # Ensure only Role Managers can access role management
4
+ before_action :ensure_role_manager
5
+ # Skip authorization for model_columns since it's just a helper endpoint
6
+ skip_authorization_check only: [:model_columns]
7
+
8
+ before_action :set_role, only: [:show, :edit, :update, :destroy]
9
+
10
+ def index
11
+ @roles = Cccux::Role.includes(:ability_permissions, :users)
12
+ .order(:priority, :name)
13
+ end
14
+
15
+ def show
16
+ @permission_matrix = build_permission_matrix
17
+ @users_with_role = @role.users
18
+ end
19
+
20
+ def new
21
+ @role = Cccux::Role.new(priority: 50)
22
+ end
23
+
24
+ def create
25
+ @role = Cccux::Role.new(role_params)
26
+
27
+ respond_to do |format|
28
+ if @role.save
29
+ format.turbo_stream do
30
+ render turbo_stream: [
31
+ turbo_stream.update("new_role_form", ""),
32
+ turbo_stream.append("roles_list", partial: "role", locals: { role: @role }),
33
+ turbo_stream.update("flash", partial: "flash", locals: { notice: "Role was successfully created." })
34
+ ]
35
+ end
36
+ format.html { redirect_to cccux.role_path(@role), notice: 'Role was successfully created.' }
37
+ else
38
+ format.turbo_stream do
39
+ render turbo_stream: turbo_stream.update("new_role_form",
40
+ partial: "form", locals: { role: @role })
41
+ end
42
+ format.html { render :new, status: :unprocessable_entity }
43
+ end
44
+ end
45
+ end
46
+
47
+ def edit
48
+ @permission_matrix = build_permission_matrix
49
+ @available_permissions = Cccux::AbilityPermission.all.group_by(&:subject)
50
+ @available_ownership_models = discover_application_models
51
+ end
52
+
53
+ def update
54
+ @available_ownership_models = discover_application_models
55
+ if @role.update(role_params)
56
+ update_permissions
57
+ redirect_to cccux.roles_path, notice: 'Role was successfully updated.'
58
+ else
59
+ @permission_matrix = build_permission_matrix
60
+ @available_permissions = Cccux::AbilityPermission.all.group_by(&:subject)
61
+ render :edit, status: :unprocessable_entity
62
+ end
63
+ end
64
+
65
+ def destroy
66
+ respond_to do |format|
67
+ if @role.users.any?
68
+ format.turbo_stream do
69
+ render turbo_stream: turbo_stream.update("flash",
70
+ partial: "flash", locals: { alert: "Cannot delete role that has users assigned to it." })
71
+ end
72
+ format.html { redirect_to cccux.roles_path, alert: 'Cannot delete role that has users assigned to it.' }
73
+ else
74
+ @role.destroy
75
+ format.turbo_stream do
76
+ render turbo_stream: [
77
+ turbo_stream.remove("role_#{@role.id}"),
78
+ turbo_stream.update("flash", partial: "flash", locals: { notice: "Role was successfully deleted." })
79
+ ]
80
+ end
81
+ format.html { redirect_to cccux.roles_path, notice: 'Role was successfully deleted.' }
82
+ end
83
+ end
84
+ end
85
+
86
+ def permissions
87
+ @permission_matrix = build_permission_matrix
88
+ end
89
+
90
+ def reorder
91
+ role_ids = params[:role_ids]
92
+
93
+ if role_ids.present?
94
+ # Update priorities based on order (first item gets priority 1, second gets 10, etc.)
95
+ role_ids.each_with_index do |role_id, index|
96
+ role = Cccux::Role.find(role_id)
97
+ new_priority = (index + 1) * 10 # 10, 20, 30, 40, etc.
98
+ role.update!(priority: new_priority)
99
+ end
100
+
101
+ render json: { success: true, message: 'Role priorities updated successfully' }
102
+ else
103
+ render json: { success: false, error: 'No role order provided' }, status: :bad_request
104
+ end
105
+ rescue StandardError => e
106
+ render json: { success: false, error: e.message }, status: :unprocessable_entity
107
+ end
108
+
109
+ def model_columns
110
+ model_name = params[:model]
111
+ Rails.logger.info "CCCUX: Fetching columns for model: #{model_name}"
112
+
113
+ begin
114
+ model_class = model_name.constantize
115
+ columns = model_class.column_names
116
+ Rails.logger.info "CCCUX: Found columns for #{model_name}: #{columns.inspect}"
117
+ render json: { columns: columns }
118
+ rescue NameError => e
119
+ Rails.logger.warn "CCCUX: Model not found: #{model_name}"
120
+ render json: { columns: [], error: "Model '#{model_name}' not found" }, status: 422
121
+ rescue => e
122
+ Rails.logger.error "CCCUX: Error fetching columns for #{model_name}: #{e.message}"
123
+ render json: { columns: [], error: e.message }, status: 422
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def set_role
130
+ @role = Cccux::Role.find(params[:id])
131
+ end
132
+
133
+ def build_permission_matrix
134
+ subjects = Cccux::AbilityPermission.distinct.pluck(:subject).sort
135
+ actions = Cccux::AbilityPermission.distinct.pluck(:action).sort
136
+
137
+ matrix = {}
138
+ subjects.each do |subject|
139
+ matrix[subject] = {}
140
+ actions.each do |action|
141
+ permission = Cccux::AbilityPermission.find_by(action: action, subject: subject)
142
+ matrix[subject][action] = {
143
+ permission: permission,
144
+ granted: permission && @role.ability_permissions.include?(permission)
145
+ }
146
+ end
147
+ end
148
+ matrix
149
+ end
150
+
151
+ def update_permissions
152
+ permission_ids = params[:role][:ability_permission_ids] || []
153
+ permission_access_type = params[:role][:permission_access_type] || {}
154
+ ownership_source = params[:role][:ownership_source] || {}
155
+ ownership_foreign_key = params[:role][:ownership_foreign_key] || {}
156
+ ownership_user_key = params[:role][:ownership_user_key] || {}
157
+
158
+ # Get selected permissions
159
+ selected_permissions = Cccux::AbilityPermission.where(id: permission_ids)
160
+
161
+ # Remove permissions that are no longer selected
162
+ @role.role_abilities.where.not(ability_permission: selected_permissions).destroy_all
163
+
164
+ # Update or create role abilities with access type settings
165
+ selected_permissions.each do |permission|
166
+ # Determine access type for this specific permission
167
+ # Default to 'global' for backward compatibility
168
+ access_type = permission_access_type[permission.id.to_s] || 'global'
169
+
170
+ # Convert access_type to owned/context attributes
171
+ case access_type
172
+ when 'owned'
173
+ is_owned = true
174
+ context_value = 'owned'
175
+ else # 'global'
176
+ is_owned = false
177
+ context_value = 'global'
178
+ end
179
+
180
+ # Find existing role ability or create new one
181
+ role_ability = @role.role_abilities.find_or_initialize_by(ability_permission: permission)
182
+ role_ability.owned = is_owned
183
+ role_ability.context = context_value
184
+
185
+ # Handle ownership configuration for 'owned' access type
186
+ if access_type == 'owned'
187
+ # Set ownership source if provided
188
+ if ownership_source[permission.id.to_s].present?
189
+ role_ability.ownership_source = ownership_source[permission.id.to_s]
190
+ else
191
+ role_ability.ownership_source = nil
192
+ end
193
+
194
+ # Build ownership conditions JSON
195
+ conditions = {}
196
+ if ownership_foreign_key[permission.id.to_s].present?
197
+ conditions["foreign_key"] = ownership_foreign_key[permission.id.to_s]
198
+ end
199
+ if ownership_user_key[permission.id.to_s].present?
200
+ conditions["user_key"] = ownership_user_key[permission.id.to_s]
201
+ end
202
+
203
+ if conditions.any?
204
+ role_ability.ownership_conditions = conditions.to_json
205
+ else
206
+ role_ability.ownership_conditions = nil
207
+ end
208
+ else
209
+ # Clear ownership configuration for non-owned access types
210
+ role_ability.ownership_source = nil
211
+ role_ability.ownership_conditions = nil
212
+ end
213
+
214
+ role_ability.save!
215
+ end
216
+ end
217
+
218
+ def role_params
219
+ params.require(:role).permit(:name, :description, :active, :priority)
220
+ end
221
+
222
+ def discover_application_models
223
+ models = []
224
+ begin
225
+ # Direct approach: Get models from database tables (bypasses all autoloading issues)
226
+ application_tables = ActiveRecord::Base.connection.tables.reject do |table|
227
+ # Skip Rails internal tables and CCCUX tables
228
+ table.start_with?('schema_migrations', 'ar_internal_metadata', 'cccux_') ||
229
+ skip_table?(table)
230
+ end
231
+ application_tables.each do |table|
232
+ # Convert table name to model name
233
+ model_name = table.singularize.camelize
234
+ # Verify the model exists and is valid
235
+ begin
236
+ if Object.const_defined?(model_name)
237
+ model_class = Object.const_get(model_name)
238
+ if model_class.respond_to?(:table_name) &&
239
+ model_class.table_name == table &&
240
+ !skip_model_by_name?(model_name)
241
+ models << model_name
242
+ end
243
+ else
244
+ # Model constant doesn't exist yet, but table does - likely a valid model
245
+ unless skip_model_by_name?(model_name)
246
+ models << model_name
247
+ end
248
+ end
249
+ rescue => e
250
+ # Ignore
251
+ end
252
+ end
253
+ # Always include CCCUX engine models for management (but not User since host app owns it)
254
+ cccux_models = %w[Cccux::Role Cccux::AbilityPermission Cccux::UserRole Cccux::RoleAbility]
255
+ models += cccux_models
256
+ rescue => e
257
+ Rails.logger.warn "Error discovering models: #{e.message}"
258
+ Rails.logger.warn e.backtrace.join("\n")
259
+ end
260
+ models.uniq.sort
261
+ end
262
+
263
+ def skip_model_by_name?(model_name)
264
+ excluded_patterns = [
265
+ /^ActiveRecord::/,
266
+ /^ActiveStorage::/,
267
+ /^ActionText::/,
268
+ /^ActionMailbox::/,
269
+ /^ApplicationRecord$/,
270
+ /Version$/,
271
+ /Schema/,
272
+ /Migration/
273
+ ]
274
+ excluded_patterns.any? { |pattern| model_name.match?(pattern) }
275
+ end
276
+
277
+ def skip_table?(table_name)
278
+ excluded_tables = [
279
+ 'active_storage_blobs',
280
+ 'active_storage_attachments',
281
+ 'active_storage_variant_records',
282
+ 'action_text_rich_texts',
283
+ 'action_mailbox_inbound_emails',
284
+ 'versions'
285
+ ]
286
+ excluded_tables.include?(table_name) ||
287
+ table_name.end_with?('_versions')
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,7 @@
1
+ module Cccux
2
+ class SimpleController < ApplicationController
3
+ def index
4
+ render plain: "CCCUX Engine is working! Time: #{Time.current}"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,112 @@
1
+ class Cccux::UsersController < Cccux::CccuxController
2
+ # Ensure only Role Managers can access user management
3
+ before_action :ensure_role_manager
4
+
5
+ before_action :set_user, only: [:show, :edit, :update, :destroy]
6
+
7
+ def index
8
+ @users = User.includes(:cccux_roles).order(:email)
9
+ @roles = Cccux::Role.active.order(:name)
10
+ end
11
+
12
+ def show
13
+ @user_roles = @user.cccux_roles.includes(:ability_permissions)
14
+ @available_roles = Cccux::Role.active.where.not(id: @user.cccux_roles.pluck(:id))
15
+ end
16
+
17
+ def new
18
+ @user = User.new
19
+ @roles = Cccux::Role.active.order(:name)
20
+ end
21
+
22
+ def create
23
+ @user = User.new(user_params)
24
+
25
+ if @user.save
26
+ # Assign selected roles
27
+ if params[:user][:role_ids].present?
28
+ params[:user][:role_ids].reject(&:blank?).each do |role_id|
29
+ role = Cccux::Role.find(role_id)
30
+ @user.assign_role(role)
31
+ end
32
+ end
33
+
34
+ redirect_to cccux.user_path(@user), notice: 'User was successfully created.'
35
+ else
36
+ @roles = Cccux::Role.active.order(:name)
37
+ render :new, status: :unprocessable_entity
38
+ end
39
+ end
40
+
41
+ def edit
42
+ @available_roles = Cccux::Role.active.order(:name)
43
+ @user_role_ids = @user.cccux_roles.pluck(:id)
44
+ end
45
+
46
+ def update
47
+ # Handle password updates - remove blank password fields
48
+ update_params = user_params
49
+ if update_params[:password].blank?
50
+ update_params = update_params.except(:password, :password_confirmation)
51
+ end
52
+
53
+ if @user.update(update_params)
54
+ # Update role assignments
55
+ if params[:user][:role_ids]
56
+ # Remove all current roles
57
+ @user.cccux_user_roles.destroy_all
58
+
59
+ # Add selected roles
60
+ params[:user][:role_ids].reject(&:blank?).each do |role_id|
61
+ role = Cccux::Role.find(role_id)
62
+ @user.assign_role(role)
63
+ end
64
+ end
65
+
66
+ redirect_to cccux.user_path(@user), notice: 'User was successfully updated.'
67
+ else
68
+ @available_roles = Cccux::Role.active.order(:name)
69
+ @user_role_ids = @user.cccux_roles.pluck(:id)
70
+ render :edit, status: :unprocessable_entity
71
+ end
72
+ end
73
+
74
+ def destroy
75
+ @user.destroy
76
+ redirect_to cccux.users_path, notice: 'User was successfully deleted.'
77
+ end
78
+
79
+ # AJAX endpoint for role assignment
80
+ def assign_role
81
+ @user = User.find(params[:id])
82
+ role = Cccux::Role.find(params[:role_id])
83
+
84
+ if @user.assign_role(role)
85
+ render json: { status: 'success', message: "#{role.name} role assigned to #{@user.email}" }
86
+ else
87
+ render json: { status: 'error', message: 'Failed to assign role' }
88
+ end
89
+ end
90
+
91
+ # AJAX endpoint for role removal
92
+ def remove_role
93
+ @user = User.find(params[:id])
94
+ role = Cccux::Role.find(params[:role_id])
95
+
96
+ if @user.remove_role(role)
97
+ render json: { status: 'success', message: "#{role.name} role removed from #{@user.email}" }
98
+ else
99
+ render json: { status: 'error', message: 'Failed to remove role' }
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def set_user
106
+ @user = User.find(params[:id])
107
+ end
108
+
109
+ def user_params
110
+ params.require(:user).permit(:email, :password, :password_confirmation)
111
+ end
112
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cccux
4
+ module ApplicationControllerConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # CanCanCan integration for authorization
9
+ include CanCan::ControllerAdditions
10
+
11
+ # Include CCCUX helpers for views
12
+ helper Cccux::AuthorizationHelper
13
+
14
+ # Handle CanCanCan authorization errors gracefully
15
+ rescue_from CanCan::AccessDenied do |exception|
16
+ redirect_to root_path, alert: 'Access denied.'
17
+ end
18
+
19
+ # Handle 404 errors gracefully
20
+ rescue_from ActiveRecord::RecordNotFound do |exception|
21
+ redirect_to root_path, alert: 'The requested resource was not found.'
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ # Override current_ability to use CCCUX Ability class
28
+ def current_ability
29
+ @current_ability ||= Cccux::Ability.new(current_user)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module Cccux
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,228 @@
1
+ module Cccux
2
+ module AuthorizationHelper
3
+ # Link helpers for common actions
4
+ def link_if_can_index(subject, text, path, **opts)
5
+ link_to(text, path, **opts) if can?(:index, subject)
6
+ end
7
+
8
+ def link_if_can_show(subject, text, path, **opts)
9
+ link_to(text, path, **opts) if can?(:show, subject)
10
+ end
11
+
12
+ def link_if_can_create(subject, text, path, **opts)
13
+ link_to(text, path, **opts) if can?(:create, subject)
14
+ end
15
+
16
+ def link_if_can_edit(subject, text, path, **opts)
17
+ link_to(text, path, **opts) if can?(:edit, subject)
18
+ end
19
+
20
+ def link_if_can_update(subject, text, path, **opts)
21
+ link_to(text, path, **opts) if can?(:update, subject)
22
+ end
23
+
24
+ def link_if_can_destroy(subject, text, path, **opts)
25
+ link_to(text, path, **opts) if can?(:destroy, subject)
26
+ end
27
+
28
+ # Button helpers for common actions
29
+ def button_if_can_index(subject, text, path, **opts)
30
+ button_to(text, path, **opts) if can?(:index, subject)
31
+ end
32
+
33
+ def button_if_can_show(subject, text, path, **opts)
34
+ button_to(text, path, **opts) if can?(:show, subject)
35
+ end
36
+
37
+ def button_if_can_create(subject, text, path, **opts)
38
+ button_to(text, path, **opts) if can?(:create, subject)
39
+ end
40
+
41
+ def button_if_can_edit(subject, text, path, **opts)
42
+ button_to(text, path, **opts) if can?(:edit, subject)
43
+ end
44
+
45
+ def button_if_can_update(subject, text, path, **opts)
46
+ button_to(text, path, **opts) if can?(:update, subject)
47
+ end
48
+
49
+ def button_if_can_destroy(subject, text, path, **opts)
50
+ button_to(text, path, **opts) if can?(:destroy, subject)
51
+ end
52
+
53
+ # Generic action helpers
54
+ def link_if_can(action, subject, text, path, **opts)
55
+ link_to(text, path, **opts) if can?(action, subject)
56
+ end
57
+
58
+ def button_if_can(action, subject, text, path, **opts)
59
+ button_to(text, path, **opts) if can?(action, subject)
60
+ end
61
+
62
+ # Content helpers for conditional rendering
63
+ def content_if_can(action, subject, &block)
64
+ capture(&block) if can?(action, subject)
65
+ end
66
+
67
+ def content_if_can_index(subject, &block)
68
+ content_if_can(:index, subject, &block)
69
+ end
70
+
71
+ def content_if_can_show(subject, &block)
72
+ content_if_can(:show, subject, &block)
73
+ end
74
+
75
+ def content_if_can_create(subject, &block)
76
+ content_if_can(:create, subject, &block)
77
+ end
78
+
79
+ def content_if_can_edit(subject, &block)
80
+ content_if_can(:edit, subject, &block)
81
+ end
82
+
83
+ def content_if_can_update(subject, &block)
84
+ content_if_can(:update, subject, &block)
85
+ end
86
+
87
+ def content_if_can_destroy(subject, &block)
88
+ content_if_can(:destroy, subject, &block)
89
+ end
90
+
91
+ # Icon helpers (useful for action buttons)
92
+ def icon_link_if_can(action, subject, icon_class, text, path, **opts)
93
+ link_to(path, **opts) do
94
+ content_tag(:i, '', class: icon_class) + ' ' + text
95
+ end if can?(action, subject)
96
+ end
97
+
98
+ def icon_button_if_can(action, subject, icon_class, text, path, **opts)
99
+ button_to(path, **opts) do
100
+ content_tag(:i, '', class: icon_class) + ' ' + text
101
+ end if can?(action, subject)
102
+ end
103
+
104
+ # Common action button helpers with icons
105
+ def new_button_if_can(subject, text = "New #{subject.name.underscore.humanize}", path = nil, **opts)
106
+ path ||= "new_#{subject.name.underscore}_path"
107
+ icon_button_if_can(:create, subject, 'fas fa-plus', text, path, **opts)
108
+ end
109
+
110
+ def edit_button_if_can(subject, text = "Edit", path = nil, **opts)
111
+ path ||= "edit_#{subject.name.underscore}_path(subject)"
112
+ icon_button_if_can(:edit, subject, 'fas fa-edit', text, path, **opts)
113
+ end
114
+
115
+ def delete_button_if_can(subject, text = "Delete", path = nil, **opts)
116
+ path ||= "#{subject.name.underscore}_path(subject)"
117
+ opts[:method] ||= :delete
118
+ opts[:data] ||= {}
119
+ opts[:data][:confirm] ||= "Are you sure?"
120
+ icon_button_if_can(:destroy, subject, 'fas fa-trash', text, path, **opts)
121
+ end
122
+
123
+ def view_button_if_can(subject, text = "View", path = nil, **opts)
124
+ path ||= "#{subject.name.underscore}_path(subject)"
125
+ icon_button_if_can(:show, subject, 'fas fa-eye', text, path, **opts)
126
+ end
127
+
128
+ # Table action helpers
129
+ def table_actions_if_can(subject, record, **opts)
130
+ content_if_can(:show, subject) do
131
+ content_tag(:div, class: 'table-actions') do
132
+ safe_join([
133
+ view_button_if_can(subject, "View", "#{subject.name.underscore}_path(record)", **opts),
134
+ edit_button_if_can(subject, "Edit", "edit_#{subject.name.underscore}_path(record)", **opts),
135
+ delete_button_if_can(subject, "Delete", "#{subject.name.underscore}_path(record)", **opts)
136
+ ].compact)
137
+ end
138
+ end
139
+ end
140
+
141
+ # Permission check helpers
142
+ def can_index?(subject)
143
+ can?(:index, subject)
144
+ end
145
+
146
+ def can_show?(subject)
147
+ can?(:show, subject)
148
+ end
149
+
150
+ def can_create?(subject)
151
+ can?(:create, subject)
152
+ end
153
+
154
+ def can_edit?(subject)
155
+ can?(:edit, subject)
156
+ end
157
+
158
+ def can_update?(subject)
159
+ can?(:update, subject)
160
+ end
161
+
162
+ def can_destroy?(subject)
163
+ can?(:destroy, subject)
164
+ end
165
+
166
+ # Check if user can perform action on resource in global context
167
+ def can_in_global_context?(action, resource)
168
+ current_ability.can?(action, resource) &&
169
+ has_context_permission?(action, resource.class, 'global')
170
+ end
171
+
172
+ # Check if user can perform action on resource in owned context
173
+ def can_in_owned_context?(action, resource)
174
+ current_ability.can?(action, resource) &&
175
+ has_context_permission?(action, resource.class, 'owned')
176
+ end
177
+
178
+ # Check if user can perform action on resource in scoped context
179
+ def can_in_scoped_context?(action, resource)
180
+ current_ability.can?(action, resource) &&
181
+ has_context_permission?(action, resource.class, 'scoped')
182
+ end
183
+
184
+ # Check if user can access the current route context
185
+ def can_access_current_context?(action, resource_class)
186
+ context = determine_current_context
187
+ case context
188
+ when 'global'
189
+ can_in_global_context?(action, resource_class)
190
+ when 'owned'
191
+ can_in_owned_context?(action, resource_class)
192
+ when 'scoped'
193
+ can_in_scoped_context?(action, resource_class)
194
+ else
195
+ current_ability.can?(action, resource_class)
196
+ end
197
+ end
198
+
199
+ # Determine the current route context
200
+ def determine_current_context
201
+ # Check if we're in a nested route (e.g., /store/1/orders)
202
+ if params[:store_id].present?
203
+ 'scoped'
204
+ elsif params[:user_id].present?
205
+ 'owned'
206
+ else
207
+ 'global'
208
+ end
209
+ end
210
+
211
+ private
212
+
213
+ def has_context_permission?(action, resource_class, context)
214
+ return false unless current_user&.persisted?
215
+
216
+ # Check if user has the permission in the specified context
217
+ user_roles = Cccux::UserRole.active.for_user(current_user).includes(:role)
218
+
219
+ user_roles.any? do |user_role|
220
+ role = user_role.role
221
+ role.role_abilities.joins(:ability_permission)
222
+ .where(cccux_ability_permissions: { action: action, subject: resource_class.name, active: true })
223
+ .where(context: context)
224
+ .exists?
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,4 @@
1
+ module Cccux
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Cccux
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end