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,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cccux
4
+ class Ability
5
+ include CanCan::Ability
6
+
7
+ def initialize(user, context = nil)
8
+ user ||= User.new # guest user (not logged in)
9
+ @context = context || {}
10
+
11
+ # Track which permissions have been defined to avoid conflicts
12
+ @defined_permissions = Set.new
13
+
14
+ if user.persisted?
15
+ # Authenticated user - use their assigned roles in priority order
16
+ user_roles = Cccux::UserRole.active.for_user(user).includes(:role).joins(:role).order('cccux_roles.priority DESC')
17
+ user_roles.each do |user_role|
18
+ role = user_role.role
19
+ apply_role_abilities(role, user)
20
+ end
21
+ else
22
+ # Guest user (not logged in) - use "Guest" role permissions
23
+ guest_role = Cccux::Role.find_by(name: 'Guest')
24
+ if guest_role
25
+ apply_role_abilities(guest_role, user)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def apply_role_abilities(role, user)
33
+ return unless role
34
+
35
+ # Get all abilities for this role
36
+ role_abilities = Cccux::RoleAbility.includes(:ability_permission)
37
+ .where(role: role)
38
+ .joins(:ability_permission)
39
+ .where(cccux_ability_permissions: { active: true })
40
+
41
+ role_abilities.each do |role_ability|
42
+ permission = role_ability.ability_permission
43
+
44
+ # Determine the model class
45
+ model_class = resolve_model_class(permission.subject)
46
+ next unless model_class
47
+
48
+ # Check if this permission has already been defined by a higher priority role
49
+ permission_key = "#{permission.action}:#{permission.subject}:#{role_ability.context}"
50
+ next if @defined_permissions.include?(permission_key)
51
+
52
+ # Mark this permission as defined
53
+ @defined_permissions.add(permission_key)
54
+
55
+ # Define the ability based on context and ownership
56
+ apply_access_ability(role_ability, permission, model_class, user)
57
+ end
58
+ end
59
+
60
+ def apply_access_ability(role_ability, permission, model_class, user)
61
+ action = permission.action.to_sym
62
+
63
+ # For User resource, keep owned logic for now
64
+ 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)
67
+ else
68
+ can action, model_class
69
+ end
70
+ 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)
77
+ 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
81
+ end
82
+ end
83
+ end
84
+
85
+ def apply_owned_ability(action, model_class, user, role_ability = nil)
86
+ # 1. Dynamic ownership configuration
87
+ if role_ability && role_ability.ownership_source.present?
88
+ ownership_model = role_ability.ownership_source.constantize rescue nil
89
+ if ownership_model && user&.persisted?
90
+ # Parse conditions (should be a JSON string or nil)
91
+ conditions = role_ability.ownership_conditions.present? ? JSON.parse(role_ability.ownership_conditions) : {}
92
+ foreign_key = conditions["foreign_key"] || (model_class.name.foreign_key)
93
+ 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?
97
+ else
98
+ Rails.logger.warn "CCCUX: Invalid ownership_source #{role_ability.ownership_source} for #{model_class.name}"
99
+ can action, model_class, id: []
100
+ end
101
+ # 2. Model custom owned_by?
102
+ elsif model_class.respond_to?(:owned_by?)
103
+ can action, model_class do |record|
104
+ record.owned_by?(user)
105
+ end
106
+ # 3. Model custom scoped_for_user
107
+ elsif model_class.respond_to?(:scoped_for_user)
108
+ scoped_records = model_class.scoped_for_user(user)
109
+ if scoped_records.is_a?(ActiveRecord::Relation)
110
+ ids = scoped_records.pluck(:id)
111
+ can action, model_class, id: ids if ids.any?
112
+ else
113
+ can action, model_class, scoped_records
114
+ end
115
+ # 4. Standard user_id
116
+ elsif model_class.column_names.include?('user_id')
117
+ can action, model_class, user_id: user.id
118
+ # 5. Standard creator_id
119
+ elsif model_class.column_names.include?('creator_id')
120
+ can action, model_class, creator_id: user.id
121
+ 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
125
+ end
126
+ end
127
+
128
+ def resolve_model_class(subject)
129
+ # Handle namespaced models
130
+ if subject.include?('::')
131
+ subject.constantize
132
+ else
133
+ # Try to find the model in the host app
134
+ Object.const_get(subject)
135
+ 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
+ nil
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,61 @@
1
+ module Cccux
2
+ class AbilityPermission < ApplicationRecord
3
+ self.table_name = 'cccux_ability_permissions'
4
+
5
+ has_many :role_abilities, dependent: :destroy, class_name: 'Cccux::RoleAbility'
6
+ has_many :roles, through: :role_abilities, class_name: 'Cccux::Role'
7
+
8
+ validates :action, presence: true
9
+ validates :subject, presence: true
10
+ validates :action, uniqueness: { scope: :subject }
11
+
12
+ scope :for_subject, ->(subject) { where(subject: subject) }
13
+ scope :for_action, ->(action) { where(action: action) }
14
+
15
+ def display_name
16
+ "#{action.humanize} #{subject}"
17
+ end
18
+
19
+ def role_count
20
+ roles.count
21
+ end
22
+
23
+ # Determine if this permission supports ownership controls
24
+ def supports_ownership?
25
+ # CCCUX system models don't support ownership
26
+ return false if subject.start_with?('Cccux::')
27
+
28
+ # You could add more sophisticated logic here:
29
+ # - Check if the model has ownership methods (owned_by?, scoped_for_user)
30
+ # - Check against a configuration list
31
+ # - Check if the model is in a specific namespace
32
+
33
+ true
34
+ end
35
+
36
+ # Get the model class for this permission
37
+ def model_class
38
+ return nil unless subject.present?
39
+
40
+ if subject.include?('::')
41
+ subject.constantize
42
+ else
43
+ Object.const_get(subject)
44
+ end
45
+ rescue NameError
46
+ nil
47
+ end
48
+
49
+ # Check if the model has ownership capabilities
50
+ def model_supports_ownership?
51
+ klass = model_class
52
+ return false unless klass
53
+
54
+ # Check if model has ownership methods
55
+ klass.respond_to?(:owned_by?) ||
56
+ klass.respond_to?(:scoped_for_user) ||
57
+ klass.column_names.include?('user_id') ||
58
+ klass.column_names.include?('creator_id')
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ module Cccux
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cccux
4
+ class Role < ApplicationRecord
5
+ self.table_name = 'cccux_roles'
6
+
7
+ has_many :user_roles, dependent: :destroy, class_name: 'Cccux::UserRole'
8
+ has_many :users, through: :user_roles, class_name: 'User'
9
+ has_many :role_abilities, dependent: :destroy, class_name: 'Cccux::RoleAbility'
10
+ has_many :ability_permissions, through: :role_abilities, class_name: 'Cccux::AbilityPermission'
11
+
12
+ validates :name, presence: true, uniqueness: true
13
+ validates :priority, presence: true, numericality: { only_integer: true, greater_than: 0 }
14
+
15
+ after_initialize :set_default_priority, if: :new_record?
16
+
17
+ scope :active, -> { where(active: true) }
18
+ scope :ordered, -> { order(:priority, :name) }
19
+
20
+ def self.default_roles
21
+ [
22
+ { name: 'Guest', description: 'Unauthenticated users', priority: 100 },
23
+ { name: 'Basic User', description: 'Standard authenticated users', priority: 50 },
24
+ { name: 'Role Manager', description: 'Can manage roles and permissions', priority: 25 },
25
+ { name: 'Administrator', description: 'Full system access', priority: 1 }
26
+ ]
27
+ end
28
+
29
+ def assign_permission(permission)
30
+ return false unless permission.is_a?(Cccux::AbilityPermission)
31
+
32
+ Cccux::RoleAbility.find_or_create_by(role: self, ability_permission: permission)
33
+ end
34
+
35
+ def remove_permission(permission)
36
+ return false unless permission.is_a?(Cccux::AbilityPermission)
37
+
38
+ role_abilities.where(ability_permission: permission).destroy_all
39
+ end
40
+
41
+ def has_permission?(permission)
42
+ return false unless permission.is_a?(Cccux::AbilityPermission)
43
+
44
+ role_abilities.exists?(ability_permission: permission)
45
+ end
46
+
47
+ def permission_names
48
+ ability_permissions.pluck(:action, :subject).map { |action, subject| "#{action} #{subject}" }
49
+ end
50
+
51
+ def user_count
52
+ users.count
53
+ end
54
+
55
+ def display_name
56
+ "#{name} (#{user_count} users)"
57
+ end
58
+
59
+ # Check if role has access to all records for a given subject
60
+ def has_all_records_access?(subject)
61
+ role_abilities.joins(:ability_permission)
62
+ .where(cccux_ability_permissions: { subject: subject }, owned: false)
63
+ .exists?
64
+ end
65
+
66
+ # Check if role has access to only owned records for a given subject
67
+ def has_owned_records_access?(subject)
68
+ role_abilities.joins(:ability_permission)
69
+ .where(cccux_ability_permissions: { subject: subject }, owned: true)
70
+ .exists?
71
+ end
72
+
73
+ # Get ownership scope for a subject ('all', 'owned', or nil if no permissions)
74
+ def ownership_scope_for(subject)
75
+ if has_all_records_access?(subject)
76
+ 'all'
77
+ elsif has_owned_records_access?(subject)
78
+ 'owned'
79
+ else
80
+ nil
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def set_default_priority
87
+ self.priority ||= 50
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,49 @@
1
+ module Cccux
2
+ class RoleAbility < ApplicationRecord
3
+ self.table_name = 'cccux_role_abilities'
4
+
5
+ belongs_to :role, class_name: 'Cccux::Role'
6
+ belongs_to :ability_permission, class_name: 'Cccux::AbilityPermission'
7
+
8
+ # Simplified access types: global, owned
9
+ ACCESS_TYPES = %w[global owned].freeze
10
+
11
+ validates :role_id, presence: true
12
+ validates :ability_permission_id, presence: true
13
+ validates :context, inclusion: { in: %w[global owned], allow_nil: true }
14
+ validates :owned, inclusion: { in: [true, false] }
15
+
16
+ # Ensure unique combinations of role, permission, ownership scope, and context
17
+ validates :ability_permission_id, uniqueness: {
18
+ scope: [:role_id, :owned, :context],
19
+ message: "already exists for this role, ownership scope, and context"
20
+ }
21
+
22
+ scope :global_access, -> { where(context: 'global') }
23
+ scope :owned_access, -> { where(owned: true) }
24
+
25
+ # Simplified access types: global, owned
26
+ def access_type
27
+ if owned
28
+ 'owned'
29
+ else
30
+ 'global'
31
+ end
32
+ end
33
+
34
+ def display_name
35
+ "#{role.name} - #{ability_permission.display_name}"
36
+ end
37
+
38
+ def access_description
39
+ case access_type
40
+ when 'global'
41
+ "Global access to all records"
42
+ when 'owned'
43
+ "Access to owned records or records via configured ownership relationship"
44
+ else
45
+ "Unknown access"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cccux
4
+ class UserRole < ApplicationRecord
5
+ self.table_name = 'cccux_user_roles'
6
+
7
+ belongs_to :user, class_name: 'User', foreign_key: 'user_id'
8
+ belongs_to :role, class_name: 'Cccux::Role'
9
+
10
+ validates :user_id, presence: true
11
+ validates :role_id, presence: true
12
+ validates :user_id, uniqueness: { scope: :role_id, message: 'already has this role' }
13
+
14
+ scope :active, -> { joins(:role).where(cccux_roles: { active: true }) }
15
+ scope :for_user, ->(user) { where(user: user) }
16
+ scope :with_role, ->(role) { where(role: role) }
17
+
18
+ def self.assign_role_to_user(user, role)
19
+ find_or_create_by(user: user, role: role)
20
+ end
21
+
22
+ def self.remove_role_from_user(user, role)
23
+ where(user: user, role: role).destroy_all
24
+ end
25
+
26
+ def self.user_has_role?(user, role)
27
+ exists?(user: user, role: role)
28
+ end
29
+
30
+ def self.roles_for_user(user)
31
+ joins(:role).where(user: user).pluck('cccux_roles.name')
32
+ end
33
+
34
+ def self.users_with_role(role)
35
+ joins(:user).where(role: role).pluck('users.email')
36
+ end
37
+
38
+ def display_name
39
+ "#{user.email} - #{role.name}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ module Cccux
2
+ module Authorizable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ # Convenience scope for authorized access (shorter than accessible_by(current_ability))
7
+ # Requires an explicit ability parameter for reliability
8
+ #
9
+ # Usage:
10
+ # User.owned(current_ability) # In controllers
11
+ # User.owned(ability) # When you have an ability object
12
+ #
13
+ # This ensures proper authorization regardless of thread context
14
+ def owned(ability)
15
+ if ability
16
+ accessible_by(ability)
17
+ else
18
+ # Fallback: return all records (this should be overridden in specific models if needed)
19
+ Rails.logger.warn "CCCUX: No ability provided for #{self.name}.owned scope, returning all records"
20
+ all
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cccux
4
+ module ScopedOwnership
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Ensure the model includes Cccux::Authorizable
9
+ include Cccux::Authorizable unless included_modules.include?(Cccux::Authorizable)
10
+ end
11
+
12
+ class_methods do
13
+ # Configure scoped ownership for models with multiple ownership patterns
14
+ # Example: scoped_ownership owner: :user, parent: :store, manager_through: :store_managers
15
+ def scoped_ownership(owner: :user, parent: nil, manager_through: nil, through: nil)
16
+ owner_association = owner
17
+ parent_association = parent
18
+ manager_through_association = manager_through
19
+ through_association = through
20
+
21
+ # Define ownership method
22
+ define_method :owned_by? do |user|
23
+ return false unless user&.persisted?
24
+
25
+ # Direct owner ownership
26
+ owner_owned = respond_to?("#{owner_association}_id") &&
27
+ send("#{owner_association}_id") == user.id
28
+
29
+ # Parent management ownership (if configured)
30
+ parent_owned = if parent_association && manager_through_association
31
+ if through_association
32
+ # Indirect parent relationship (e.g., through order)
33
+ related_record = send(through_association)
34
+ parent_record = related_record&.send(parent_association)
35
+ parent_record&.send(manager_through_association)&.exists?(user: user)
36
+ else
37
+ # Direct parent relationship
38
+ parent_record = send(parent_association)
39
+ parent_record&.send(manager_through_association)&.exists?(user: user)
40
+ end
41
+ else
42
+ false
43
+ end
44
+
45
+ owner_owned || parent_owned
46
+ end
47
+
48
+ # Define scoped_for_user class method
49
+ define_singleton_method :scoped_for_user do |user|
50
+ return none unless user&.persisted?
51
+
52
+ # Get records owned by user
53
+ owner_records = where("#{owner_association}_id" => user.id)
54
+
55
+ # Get records from parents managed by user (if configured)
56
+ if parent_association && manager_through_association
57
+ if through_association
58
+ # Indirect parent relationship
59
+ parent_records = joins(through_association => parent_association)
60
+ .where("#{parent_association.to_s.pluralize}" => {
61
+ id: user.send(manager_through_association).select("#{parent_association}_id")
62
+ })
63
+ else
64
+ # Direct parent relationship
65
+ parent_records = joins(parent_association)
66
+ .where("#{table_name}.#{parent_association}_id" =>
67
+ user.send(manager_through_association).select("#{parent_association}_id"))
68
+ end
69
+
70
+ # Combine with UNION for efficiency
71
+ owner_ids = owner_records.pluck(:id)
72
+ parent_ids = parent_records.pluck(:id)
73
+ where(id: (owner_ids + parent_ids).uniq)
74
+ else
75
+ # Only owner records if no parent management configured
76
+ owner_records
77
+ end
78
+ end
79
+
80
+ # Define context scope method
81
+ define_singleton_method :in_current_scope? do |record, user, context|
82
+ # Check each context key for matches
83
+ context.each do |context_key, context_value|
84
+ association_name = context_key.to_s.sub(/_id$/, '')
85
+
86
+ if through_association
87
+ # Indirect relationship - check through association
88
+ related_record = record.send(through_association)
89
+ if related_record&.respond_to?("#{association_name}_id")
90
+ return true if related_record.send("#{association_name}_id")&.to_s == context_value.to_s
91
+ end
92
+ else
93
+ # Direct relationship - check direct association
94
+ if record.respond_to?("#{association_name}_id")
95
+ return true if record.send("#{association_name}_id")&.to_s == context_value.to_s
96
+ end
97
+ end
98
+ end
99
+
100
+ false
101
+ end
102
+ end
103
+
104
+ # For models that only have owner ownership (no parent relationship)
105
+ def owner_ownership(owner: :user)
106
+ owner_association = owner
107
+
108
+ define_method :owned_by? do |user|
109
+ return false unless user&.persisted?
110
+ send("#{owner_association}_id") == user.id
111
+ end
112
+
113
+ define_singleton_method :scoped_for_user do |user|
114
+ return none unless user&.persisted?
115
+ where("#{owner_association}_id" => user.id)
116
+ end
117
+
118
+ define_singleton_method :in_current_scope? do |record, user, context|
119
+ context.each do |context_key, context_value|
120
+ association_name = context_key.to_s.sub(/_id$/, '')
121
+ if association_name == owner_association.to_s
122
+ return true if record.send("#{owner_association}_id")&.to_s == context_value.to_s
123
+ end
124
+ end
125
+ false
126
+ end
127
+ end
128
+
129
+ # For models that only have parent ownership (no direct owner relationship)
130
+ def parent_ownership(parent: :parent, manager_through: :managers, through: nil)
131
+ parent_association = parent
132
+ manager_through_association = manager_through
133
+ through_association = through
134
+
135
+ define_method :owned_by? do |user|
136
+ return false unless user&.persisted?
137
+
138
+ if through_association
139
+ related_record = send(through_association)
140
+ parent_record = related_record&.send(parent_association)
141
+ parent_record&.send(manager_through_association)&.exists?(user: user)
142
+ else
143
+ parent_record = send(parent_association)
144
+ parent_record&.send(manager_through_association)&.exists?(user: user)
145
+ end
146
+ end
147
+
148
+ define_singleton_method :scoped_for_user do |user|
149
+ return none unless user&.persisted?
150
+
151
+ if through_association
152
+ joins(through_association => parent_association)
153
+ .where("#{parent_association.to_s.pluralize}" => {
154
+ id: user.send(manager_through_association).select("#{parent_association}_id")
155
+ })
156
+ else
157
+ joins(parent_association)
158
+ .where("#{table_name}.#{parent_association}_id" =>
159
+ user.send(manager_through_association).select("#{parent_association}_id"))
160
+ end
161
+ end
162
+
163
+ define_singleton_method :in_current_scope? do |record, user, context|
164
+ context.each do |context_key, context_value|
165
+ association_name = context_key.to_s.sub(/_id$/, '')
166
+
167
+ if through_association
168
+ related_record = record.send(through_association)
169
+ if related_record&.respond_to?("#{association_name}_id")
170
+ return true if related_record.send("#{association_name}_id")&.to_s == context_value.to_s
171
+ end
172
+ else
173
+ if association_name == parent_association.to_s
174
+ return true if record.send("#{parent_association}_id")&.to_s == context_value.to_s
175
+ end
176
+ end
177
+ end
178
+ false
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cccux
4
+ module UserConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # CCCUX associations
9
+ has_many :cccux_user_roles, class_name: 'Cccux::UserRole', dependent: :destroy
10
+ has_many :cccux_roles, through: :cccux_user_roles, source: :role, class_name: 'Cccux::Role'
11
+
12
+ # Automatically assign Basic User role to new users
13
+ after_create :assign_default_role
14
+ end
15
+
16
+ # Instance methods for user authorization
17
+ def has_role?(role_name)
18
+ cccux_roles.active.exists?(name: role_name)
19
+ end
20
+
21
+ def has_any_role?(*role_names)
22
+ cccux_roles.active.where(name: role_names).exists?
23
+ end
24
+
25
+ def has_all_roles?(*role_names)
26
+ role_names.all? { |role_name| has_role?(role_name) }
27
+ end
28
+
29
+ def can?(action, subject)
30
+ # Use CanCan's ability system
31
+ ability = Cccux::Ability.new(self)
32
+ ability.can?(action, subject)
33
+ end
34
+
35
+ def cannot?(action, subject)
36
+ !can?(action, subject)
37
+ end
38
+
39
+ def assign_role(role)
40
+ return false unless role.is_a?(Cccux::Role) || role.is_a?(String)
41
+
42
+ if role.is_a?(String)
43
+ role = Cccux::Role.find_by(name: role)
44
+ return false unless role
45
+ end
46
+
47
+ Cccux::UserRole.find_or_create_by(user: self, role: role)
48
+ end
49
+
50
+ def remove_role(role)
51
+ return false unless role.is_a?(Cccux::Role) || role.is_a?(String)
52
+
53
+ if role.is_a?(String)
54
+ role = Cccux::Role.find_by(name: role)
55
+ return false unless role
56
+ end
57
+
58
+ cccux_user_roles.where(role: role).destroy_all
59
+ end
60
+
61
+ def role_names
62
+ cccux_roles.active.pluck(:name)
63
+ end
64
+
65
+ def highest_priority_role
66
+ cccux_roles.active.order(:priority).first
67
+ end
68
+
69
+ def admin?
70
+ has_role?('Administrator')
71
+ end
72
+
73
+ def role_manager?
74
+ has_role?('Role Manager')
75
+ end
76
+
77
+ private
78
+
79
+ def assign_default_role
80
+ # Only assign if user has no roles yet
81
+ if cccux_roles.empty?
82
+ basic_user_role = Cccux::Role.find_by(name: 'Basic User')
83
+ assign_role(basic_user_role) if basic_user_role
84
+ end
85
+ end
86
+ end
87
+ end