cccux 0.1.0 → 0.3.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/README.md +124 -112
  4. data/Rakefile +57 -4
  5. data/app/assets/stylesheets/cccux/application.css +96 -72
  6. data/app/controllers/cccux/ability_permissions_controller.rb +138 -33
  7. data/app/controllers/cccux/application_controller.rb +7 -0
  8. data/app/controllers/cccux/cccux_controller.rb +20 -10
  9. data/app/controllers/cccux/dashboard_controller.rb +203 -32
  10. data/app/controllers/cccux/role_abilities_controller.rb +70 -0
  11. data/app/controllers/cccux/roles_controller.rb +70 -81
  12. data/app/controllers/cccux/users_controller.rb +22 -7
  13. data/app/controllers/concerns/cccux/application_controller_concern.rb +6 -2
  14. data/app/helpers/cccux/authorization_helper.rb +29 -21
  15. data/app/models/cccux/ability.rb +83 -32
  16. data/app/models/cccux/ability_permission.rb +9 -0
  17. data/app/models/cccux/post.rb +5 -0
  18. data/app/models/cccux/role.rb +19 -1
  19. data/app/models/cccux/role_ability.rb +3 -0
  20. data/app/models/concerns/cccux/user_concern.rb +5 -2
  21. data/app/views/cccux/ability_permissions/new.html.erb +2 -2
  22. data/app/views/cccux/dashboard/model_discovery.html.erb +7 -2
  23. data/app/views/cccux/roles/_form.html.erb +24 -71
  24. data/app/views/cccux/roles/edit.html.erb +5 -5
  25. data/app/views/cccux/roles/index.html.erb +1 -8
  26. data/app/views/cccux/roles/new.html.erb +1 -3
  27. data/app/views/cccux/users/edit.html.erb +4 -4
  28. data/app/views/cccux/users/new.html.erb +30 -15
  29. data/app/views/layouts/cccux/admin.html.erb +1 -2
  30. data/app/views/shared/_footer.html.erb +1 -1
  31. data/config/routes.rb +7 -6
  32. data/lib/cccux/engine.rb +7 -6
  33. data/lib/cccux/version.rb +1 -1
  34. data/lib/cccux.rb +12 -0
  35. data/lib/tasks/cccux.rake +271 -159
  36. metadata +10 -22
@@ -1,11 +1,13 @@
1
1
  module Cccux
2
2
  class RolesController < CccuxController
3
- # Ensure only Role Managers can access role management
4
- before_action :ensure_role_manager
5
3
  # Skip authorization for model_columns since it's just a helper endpoint
6
4
  skip_authorization_check only: [:model_columns]
7
5
 
8
- before_action :set_role, only: [:show, :edit, :update, :destroy]
6
+ # Add load_and_authorize_resource to automatically load roles
7
+ load_and_authorize_resource class: Cccux::Role
8
+
9
+ # Remove manual set_role - let load_and_authorize_resource handle it
10
+ # before_action :set_role, only: [:show, :edit, :update, :destroy]
9
11
 
10
12
  def index
11
13
  @roles = Cccux::Role.includes(:ability_permissions, :users)
@@ -25,7 +27,8 @@ module Cccux
25
27
  @role = Cccux::Role.new(role_params)
26
28
 
27
29
  respond_to do |format|
28
- if @role.save
30
+ format.html { redirect_to cccux.role_path(@role), notice: 'Role was successfully created.' } if @role.save
31
+ if defined?(Turbo::StreamsChannel) && @role.save
29
32
  format.turbo_stream do
30
33
  render turbo_stream: [
31
34
  turbo_stream.update("new_role_form", ""),
@@ -33,13 +36,15 @@ module Cccux
33
36
  turbo_stream.update("flash", partial: "flash", locals: { notice: "Role was successfully created." })
34
37
  ]
35
38
  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
39
+ end
40
+ unless @role.save
42
41
  format.html { render :new, status: :unprocessable_entity }
42
+ if defined?(Turbo::StreamsChannel)
43
+ format.turbo_stream do
44
+ render turbo_stream: turbo_stream.update("new_role_form",
45
+ partial: "form", locals: { role: @role })
46
+ end
47
+ end
43
48
  end
44
49
  end
45
50
  end
@@ -65,18 +70,22 @@ module Cccux
65
70
  def destroy
66
71
  respond_to do |format|
67
72
  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." })
73
+ if defined?(Turbo::StreamsChannel)
74
+ format.turbo_stream do
75
+ render turbo_stream: turbo_stream.update("flash",
76
+ partial: "flash", locals: { alert: "Cannot delete role that has users assigned to it." })
77
+ end
71
78
  end
72
79
  format.html { redirect_to cccux.roles_path, alert: 'Cannot delete role that has users assigned to it.' }
73
80
  else
74
81
  @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
- ]
82
+ if defined?(Turbo::StreamsChannel)
83
+ format.turbo_stream do
84
+ render turbo_stream: [
85
+ turbo_stream.remove("role_#{@role.id}"),
86
+ turbo_stream.update("flash", partial: "flash", locals: { notice: "Role was successfully deleted." })
87
+ ]
88
+ end
80
89
  end
81
90
  format.html { redirect_to cccux.roles_path, notice: 'Role was successfully deleted.' }
82
91
  end
@@ -126,9 +135,10 @@ module Cccux
126
135
 
127
136
  private
128
137
 
129
- def set_role
130
- @role = Cccux::Role.find(params[:id])
131
- end
138
+ # Remove set_role method - load_and_authorize_resource handles this
139
+ # def set_role
140
+ # @role = Cccux::Role.find(params[:id])
141
+ # end
132
142
 
133
143
  def build_permission_matrix
134
144
  subjects = Cccux::AbilityPermission.distinct.pluck(:subject).sort
@@ -200,15 +210,7 @@ module Cccux
200
210
  conditions["user_key"] = ownership_user_key[permission.id.to_s]
201
211
  end
202
212
 
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
213
+ role_ability.ownership_conditions = conditions.to_json if conditions.any?
212
214
  end
213
215
 
214
216
  role_ability.save!
@@ -218,73 +220,60 @@ module Cccux
218
220
  def role_params
219
221
  params.require(:role).permit(:name, :description, :active, :priority)
220
222
  end
221
-
223
+
222
224
  def discover_application_models
223
225
  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")
226
+
227
+ # Eager load all models to ensure they're available
228
+ Rails.application.eager_load!
229
+
230
+ # Get all ActiveRecord models from the application
231
+ ActiveRecord::Base.descendants.each do |model|
232
+ model_name = model.name
233
+
234
+ # Skip if model should be excluded
235
+ next if skip_model_by_name?(model_name)
236
+
237
+ # Skip if table doesn't exist or should be excluded
238
+ table_name = model.table_name
239
+ next if table_name.blank? || skip_table?(table_name)
240
+
241
+ models << model_name
259
242
  end
260
- models.uniq.sort
243
+
244
+ # Sort by name for consistency
245
+ models.sort
261
246
  end
262
-
247
+
263
248
  def skip_model_by_name?(model_name)
264
249
  excluded_patterns = [
265
- /^ActiveRecord::/,
266
- /^ActiveStorage::/,
267
- /^ActionText::/,
268
- /^ActionMailbox::/,
269
- /^ApplicationRecord$/,
270
- /Version$/,
271
- /Schema/,
272
- /Migration/
250
+ /^HABTM_/, # Has and belongs to many join tables
251
+ /^ActiveRecord::/, # ActiveRecord internal classes
252
+ /^ActionText::/, # ActionText models
253
+ /^ActiveStorage::/, # ActiveStorage models
254
+ /^ActionMailbox::/, # ActionMailbox models
255
+ /^Cccux::/, # CCCUX engine models (we'll handle these separately)
256
+ /^ApplicationRecord$/, # Base application record
257
+ /^ApplicationController$/, # Controllers
258
+ /^ApplicationHelper$/, # Helpers
259
+ /^ApplicationMailer$/ # Mailers
273
260
  ]
261
+
274
262
  excluded_patterns.any? { |pattern| model_name.match?(pattern) }
275
263
  end
276
-
264
+
277
265
  def skip_table?(table_name)
278
266
  excluded_tables = [
267
+ 'schema_migrations',
268
+ 'ar_internal_metadata',
279
269
  'active_storage_blobs',
280
270
  'active_storage_attachments',
281
- 'active_storage_variant_records',
282
271
  'action_text_rich_texts',
283
272
  'action_mailbox_inbound_emails',
284
- 'versions'
273
+ 'action_mailbox_routing_rules'
285
274
  ]
286
- excluded_tables.include?(table_name) ||
287
- table_name.end_with?('_versions')
275
+
276
+ excluded_tables.include?(table_name) || table_name.start_with?('active_storage_') || table_name.start_with?('action_text_')
288
277
  end
289
278
  end
290
279
  end
@@ -1,11 +1,15 @@
1
1
  class Cccux::UsersController < Cccux::CccuxController
2
- # Ensure only Role Managers can access user management
3
- before_action :ensure_role_manager
2
+ # Restore load_and_authorize_resource for User
3
+ load_and_authorize_resource class: User
4
4
 
5
- before_action :set_user, only: [:show, :edit, :update, :destroy]
5
+ # Add simple authentication check - user must be signed in
6
+ before_action :require_authentication
7
+
8
+ # Remove manual set_user - let load_and_authorize_resource handle it
9
+ # before_action :set_user, only: [:show, :edit, :update, :destroy]
6
10
 
7
11
  def index
8
- @users = User.includes(:cccux_roles).order(:email)
12
+ # Do not override @users, let load_and_authorize_resource scope it
9
13
  @roles = Cccux::Role.active.order(:name)
10
14
  end
11
15
 
@@ -102,11 +106,22 @@ class Cccux::UsersController < Cccux::CccuxController
102
106
 
103
107
  private
104
108
 
105
- def set_user
106
- @user = User.find(params[:id])
109
+ def require_authentication
110
+ unless user_signed_in?
111
+ respond_to do |format|
112
+ format.html { render plain: "Access denied", status: :forbidden }
113
+ format.json { render json: { error: 'Access denied' }, status: :forbidden }
114
+ end
115
+ return
116
+ end
107
117
  end
108
118
 
119
+ # Remove set_user method - load_and_authorize_resource handles this
120
+ # def set_user
121
+ # @user = User.find(params[:id])
122
+ # end
123
+
109
124
  def user_params
110
- params.require(:user).permit(:email, :password, :password_confirmation)
125
+ params.require(:user).permit(:email, :password, :password_confirmation, :first_name, :last_name)
111
126
  end
112
127
  end
@@ -13,12 +13,16 @@ module Cccux
13
13
 
14
14
  # Handle CanCanCan authorization errors gracefully
15
15
  rescue_from CanCan::AccessDenied do |exception|
16
- redirect_to root_path, alert: 'Access denied.'
16
+ if Rails.env.test?
17
+ render plain: "Access denied", status: :forbidden
18
+ else
19
+ redirect_to cccux.root_path, alert: 'Access denied.'
20
+ end
17
21
  end
18
22
 
19
23
  # Handle 404 errors gracefully
20
24
  rescue_from ActiveRecord::RecordNotFound do |exception|
21
- redirect_to root_path, alert: 'The requested resource was not found.'
25
+ redirect_to cccux.root_path, alert: 'The requested resource was not found.'
22
26
  end
23
27
  end
24
28
 
@@ -1,67 +1,79 @@
1
1
  module Cccux
2
2
  module AuthorizationHelper
3
3
  # Link helpers for common actions
4
+ def link_if_can_with_wrapper(action, subject, text, path, **opts)
5
+ prepend = opts.delete(:prepend)
6
+ append = opts.delete(:append)
7
+ if can?(action, subject)
8
+ "#{prepend}#{link_to(text, path, **opts)}#{append}".html_safe
9
+ elsif opts.delete(:show_text)
10
+ text
11
+ else
12
+ ""
13
+ end
14
+ end
15
+
4
16
  def link_if_can_index(subject, text, path, **opts)
5
- link_to(text, path, **opts) if can?(:index, subject)
17
+ link_if_can_with_wrapper(:index, subject, text, path, **opts)
6
18
  end
7
19
 
8
20
  def link_if_can_show(subject, text, path, **opts)
9
- link_to(text, path, **opts) if can?(:show, subject)
21
+ link_if_can_with_wrapper(:show, subject, text, path, **opts)
10
22
  end
11
23
 
12
24
  def link_if_can_create(subject, text, path, **opts)
13
- link_to(text, path, **opts) if can?(:create, subject)
25
+ link_if_can_with_wrapper(:create, subject, text, path, **opts)
14
26
  end
15
27
 
16
28
  def link_if_can_edit(subject, text, path, **opts)
17
- link_to(text, path, **opts) if can?(:edit, subject)
29
+ link_if_can_with_wrapper(:edit, subject, text, path, **opts)
18
30
  end
19
31
 
20
32
  def link_if_can_update(subject, text, path, **opts)
21
- link_to(text, path, **opts) if can?(:update, subject)
33
+ link_if_can_with_wrapper(:update, subject, text, path, **opts)
22
34
  end
23
35
 
24
36
  def link_if_can_destroy(subject, text, path, **opts)
25
- link_to(text, path, **opts) if can?(:destroy, subject)
37
+ link_if_can_with_wrapper(:destroy, subject, text, path, **opts)
26
38
  end
27
39
 
28
40
  # Button helpers for common actions
29
41
  def button_if_can_index(subject, text, path, **opts)
30
- button_to(text, path, **opts) if can?(:index, subject)
42
+ can?(:index, subject) ? button_to(text, path, **opts) : ""
31
43
  end
32
44
 
33
45
  def button_if_can_show(subject, text, path, **opts)
34
- button_to(text, path, **opts) if can?(:show, subject)
46
+ can?(:show, subject) ? button_to(text, path, **opts) : ""
35
47
  end
36
48
 
37
49
  def button_if_can_create(subject, text, path, **opts)
38
- button_to(text, path, **opts) if can?(:create, subject)
50
+ can?(:create, subject) ? button_to(text, path, **opts) : ""
39
51
  end
40
52
 
41
53
  def button_if_can_edit(subject, text, path, **opts)
42
- button_to(text, path, **opts) if can?(:edit, subject)
54
+ can?(:edit, subject) ? button_to(text, path, **opts) : ""
43
55
  end
44
56
 
45
57
  def button_if_can_update(subject, text, path, **opts)
46
- button_to(text, path, **opts) if can?(:update, subject)
58
+ can?(:update, subject) ? button_to(text, path, **opts) : ""
47
59
  end
48
60
 
49
61
  def button_if_can_destroy(subject, text, path, **opts)
50
- button_to(text, path, **opts) if can?(:destroy, subject)
62
+ can?(:destroy, subject) ? button_to(text, path, **opts) : ""
51
63
  end
52
64
 
53
65
  # Generic action helpers
54
66
  def link_if_can(action, subject, text, path, **opts)
55
- link_to(text, path, **opts) if can?(action, subject)
67
+ can?(action, subject) ? link_to(text, path, **opts) : ""
56
68
  end
57
69
 
58
70
  def button_if_can(action, subject, text, path, **opts)
59
- button_to(text, path, **opts) if can?(action, subject)
71
+ can?(action, subject) ? button_to(text, path, **opts) : ""
60
72
  end
61
73
 
62
74
  # Content helpers for conditional rendering
63
75
  def content_if_can(action, subject, &block)
64
- capture(&block) if can?(action, subject)
76
+ can?(action, subject) ? capture(&block) : ""
65
77
  end
66
78
 
67
79
  def content_if_can_index(subject, &block)
@@ -90,15 +102,11 @@ module Cccux
90
102
 
91
103
  # Icon helpers (useful for action buttons)
92
104
  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)
105
+ can?(action, subject) ? link_to(path, **opts) { content_tag(:i, '', class: icon_class) + ' ' + text } : ""
96
106
  end
97
107
 
98
108
  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)
109
+ can?(action, subject) ? button_to(path, **opts) { content_tag(:i, '', class: icon_class) + ' ' + text } : ""
102
110
  end
103
111
 
104
112
  # Common action button helpers with icons
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Cccux
4
4
  class Ability
5
- include CanCan::Ability
5
+ include CanCan::Ability
6
6
 
7
- def initialize(user, context = nil)
7
+ def initialize(user, context = nil)
8
8
  user ||= User.new # guest user (not logged in)
9
9
  @context = context || {}
10
10
 
@@ -60,24 +60,33 @@ module Cccux
60
60
  def apply_access_ability(role_ability, permission, model_class, user)
61
61
  action = permission.action.to_sym
62
62
 
63
+ # Handle action aliases (read includes show and index)
64
+ actions_to_grant = case action
65
+ when :read
66
+ [:read, :show, :index]
67
+ when :update
68
+ [:update, :edit]
69
+ when :create
70
+ [:create, :new]
71
+ when :destroy
72
+ [:destroy, :delete]
73
+ else
74
+ [action]
75
+ end
76
+
63
77
  # For User resource, keep owned logic for now
64
78
  if permission.subject == 'User'
65
- if role_ability.context == 'owned' || (role_ability.owned && user&.persisted?)
66
- apply_owned_ability(action, model_class, user, role_ability)
79
+ if role_ability.context == 'owned' || role_ability.owned
80
+ actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
67
81
  else
68
- can action, model_class
82
+ actions_to_grant.each { |act| can act, model_class }
69
83
  end
70
84
  else
71
- # For all other resources, use global/owned (contextual is now handled by owned with configuration)
72
- case role_ability.access_type
73
- when 'global'
74
- can action, model_class
75
- when 'owned'
76
- apply_owned_ability(action, model_class, user, role_ability)
85
+ # For all other resources, use global/owned logic based on context field
86
+ if role_ability.context == 'owned' || role_ability.owned
87
+ actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
77
88
  else
78
- # Default: deny access if access_type is not recognized
79
- Rails.logger.warn "CCCUX: Unknown access_type '#{role_ability.access_type}' for #{model_class.name}, denying access"
80
- # Don't grant any permissions - CanCanCan denies by default
89
+ actions_to_grant.each { |act| can act, model_class }
81
90
  end
82
91
  end
83
92
  end
@@ -89,13 +98,31 @@ module Cccux
89
98
  if ownership_model && user&.persisted?
90
99
  # Parse conditions (should be a JSON string or nil)
91
100
  conditions = role_ability.ownership_conditions.present? ? JSON.parse(role_ability.ownership_conditions) : {}
92
- foreign_key = conditions["foreign_key"] || (model_class.name.foreign_key)
101
+ foreign_key = conditions["foreign_key"]
93
102
  user_key = conditions["user_key"] || "user_id"
94
- # Find all records owned by user via the join model
95
- owned_ids = ownership_model.where(user_key => user.id).pluck(foreign_key)
96
- can action, model_class, id: owned_ids if owned_ids.any?
103
+
104
+ # Require foreign_key to be explicitly specified when using ownership model
105
+ if foreign_key.present?
106
+ # Find all records owned by user via the join model
107
+ owned_ids = ownership_model.where(user_key => user.id).pluck(foreign_key)
108
+ if owned_ids.any?
109
+ # Check if the target model has the foreign key column
110
+ if model_class.column_names.include?(foreign_key)
111
+ # Direct ownership: model has the foreign key (e.g., Comment has post_id)
112
+ can action, model_class, foreign_key.to_sym => owned_ids
113
+ else
114
+ # Indirect ownership: model doesn't have the foreign key, use primary key
115
+ # This means the foreign key in the join table refers to the target model's primary key
116
+ can action, model_class, id: owned_ids
117
+ end
118
+ else
119
+ can action, model_class, id: []
120
+ end
121
+ else
122
+ # Fall back to no access if foreign_key is not specified
123
+ can action, model_class, id: []
124
+ end
97
125
  else
98
- Rails.logger.warn "CCCUX: Invalid ownership_source #{role_ability.ownership_source} for #{model_class.name}"
99
126
  can action, model_class, id: []
100
127
  end
101
128
  # 2. Model custom owned_by?
@@ -112,30 +139,54 @@ module Cccux
112
139
  else
113
140
  can action, model_class, scoped_records
114
141
  end
115
- # 4. Standard user_id
142
+ # 4. Special case for User model (self-ownership)
143
+ elsif model_class == User
144
+ can action, model_class, id: user.id
145
+ # 5. Standard user_id
116
146
  elsif model_class.column_names.include?('user_id')
117
147
  can action, model_class, user_id: user.id
118
- # 5. Standard creator_id
148
+ # 6. Standard creator_id
119
149
  elsif model_class.column_names.include?('creator_id')
120
150
  can action, model_class, creator_id: user.id
151
+ # 7. Dynamic ownership check for individual records
121
152
  else
122
- # Default: deny access when no ownership pattern is found
123
- Rails.logger.warn "CCCUX: No ownership pattern found for #{model_class.name}, denying access"
124
- # Don't grant any permissions - CanCanCan denies by default
153
+ # For cases where we need to check individual record attributes
154
+ can action, model_class do |record|
155
+ if record.respond_to?(:creator_id)
156
+ record.creator_id == user.id
157
+ elsif record.respond_to?(:user_id)
158
+ record.user_id == user.id
159
+ else
160
+ false
161
+ end
162
+ end
125
163
  end
126
164
  end
127
165
 
128
166
  def resolve_model_class(subject)
129
- # Handle namespaced models
130
- if subject.include?('::')
131
- subject.constantize
167
+ # Try to resolve the model class in a robust way
168
+ candidates = []
169
+ if subject.include?("::")
170
+ candidates << subject
171
+ candidates << subject.split("::").last
132
172
  else
133
- # Try to find the model in the host app
134
- Object.const_get(subject)
173
+ candidates << subject
174
+ candidates << "Cccux::#{subject}"
175
+
176
+ end
177
+
178
+ # Add more candidates for common patterns
179
+ candidates << subject.split("::").last if subject.include?("::")
180
+ candidates << subject.gsub("Cccux::", "") if subject.start_with?("Cccux::")
181
+
182
+ candidates.each do |candidate|
183
+ begin
184
+ klass = candidate.constantize
185
+ return klass
186
+ rescue NameError => e
187
+ next
188
+ end
135
189
  end
136
- rescue NameError
137
- # If the model doesn't exist, we can't define permissions for it
138
- Rails.logger.warn "CCCUX: Model '#{subject}' not found, skipping permission"
139
190
  nil
140
191
  end
141
192
  end
@@ -12,6 +12,9 @@ module Cccux
12
12
  scope :for_subject, ->(subject) { where(subject: subject) }
13
13
  scope :for_action, ->(action) { where(action: action) }
14
14
 
15
+ # Ensure all permissions are created as active by default
16
+ before_create :ensure_active
17
+
15
18
  def display_name
16
19
  "#{action.humanize} #{subject}"
17
20
  end
@@ -57,5 +60,11 @@ module Cccux
57
60
  klass.column_names.include?('user_id') ||
58
61
  klass.column_names.include?('creator_id')
59
62
  end
63
+
64
+ private
65
+
66
+ def ensure_active
67
+ self.active = true if active.nil?
68
+ end
60
69
  end
61
70
  end
@@ -0,0 +1,5 @@
1
+ module Cccux
2
+ class Post < ApplicationRecord
3
+ belongs_to :user
4
+ end
5
+ end
@@ -9,10 +9,11 @@ module Cccux
9
9
  has_many :role_abilities, dependent: :destroy, class_name: 'Cccux::RoleAbility'
10
10
  has_many :ability_permissions, through: :role_abilities, class_name: 'Cccux::AbilityPermission'
11
11
 
12
- validates :name, presence: true, uniqueness: true
12
+ validates :name, presence: true
13
13
  validates :priority, presence: true, numericality: { only_integer: true, greater_than: 0 }
14
14
 
15
15
  after_initialize :set_default_priority, if: :new_record?
16
+ validate :name_uniqueness_case_insensitive
16
17
 
17
18
  scope :active, -> { where(active: true) }
18
19
  scope :ordered, -> { order(:priority, :name) }
@@ -81,6 +82,23 @@ module Cccux
81
82
  end
82
83
  end
83
84
 
85
+ # Generate slug from name
86
+ def slug
87
+ name.parameterize.underscore if name.present?
88
+ end
89
+
90
+ # Case insensitive name validation
91
+ def name_uniqueness_case_insensitive
92
+ return unless name.present?
93
+
94
+ existing_role = self.class.where('LOWER(name) = ?', name.downcase)
95
+ existing_role = existing_role.where.not(id: id) if persisted?
96
+
97
+ if existing_role.exists?
98
+ errors.add(:name, 'has already been taken')
99
+ end
100
+ end
101
+
84
102
  private
85
103
 
86
104
  def set_default_priority
@@ -5,6 +5,9 @@ module Cccux
5
5
  belongs_to :role, class_name: 'Cccux::Role'
6
6
  belongs_to :ability_permission, class_name: 'Cccux::AbilityPermission'
7
7
 
8
+ # Delegate methods to ability_permission
9
+ delegate :action, :subject, :active?, to: :ability_permission, allow_nil: true
10
+
8
11
  # Simplified access types: global, owned
9
12
  ACCESS_TYPES = %w[global owned].freeze
10
13
 
@@ -9,13 +9,16 @@ module Cccux
9
9
  has_many :cccux_user_roles, class_name: 'Cccux::UserRole', dependent: :destroy
10
10
  has_many :cccux_roles, through: :cccux_user_roles, source: :role, class_name: 'Cccux::Role'
11
11
 
12
+ # Alias for easier access
13
+ alias_method :roles, :cccux_roles
14
+
12
15
  # Automatically assign Basic User role to new users
13
16
  after_create :assign_default_role
14
17
  end
15
18
 
16
19
  # Instance methods for user authorization
17
20
  def has_role?(role_name)
18
- cccux_roles.active.exists?(name: role_name)
21
+ cccux_user_roles.active.joins(:role).where(cccux_roles: { name: role_name }).exists?
19
22
  end
20
23
 
21
24
  def has_any_role?(*role_names)
@@ -59,7 +62,7 @@ module Cccux
59
62
  end
60
63
 
61
64
  def role_names
62
- cccux_roles.active.pluck(:name)
65
+ cccux_user_roles.active.joins(:role).pluck('cccux_roles.name')
63
66
  end
64
67
 
65
68
  def highest_priority_role