cccux 0.1.0 → 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.
@@ -4,7 +4,11 @@ module Cccux
4
4
 
5
5
  # Override the default error message for admin interface
6
6
  rescue_from CanCan::AccessDenied do |exception|
7
- redirect_to main_app.root_path, alert: 'Access denied. Only Role Managers can access the admin interface.'
7
+ respond_to do |format|
8
+ format.html { render file: Rails.root.join('public', '403.html'), status: :forbidden, layout: false }
9
+ format.json { render json: { error: 'Access denied' }, status: :forbidden }
10
+ format.any { head :forbidden }
11
+ end
8
12
  end
9
13
 
10
14
  rescue_from ActionController::RoutingError do |exception|
@@ -19,7 +23,7 @@ module Cccux
19
23
  # Automatically load and authorize resources for all actions
20
24
  # This works because CCCUX provides default roles (Guest, Basic User)
21
25
  # so every user has permissions to check against
22
- load_and_authorize_resource
26
+ # load_and_authorize_resource # Commented out to avoid conflicts with child controllers
23
27
 
24
28
  protected
25
29
 
@@ -42,20 +46,26 @@ module Cccux
42
46
  private
43
47
 
44
48
  def ensure_role_manager
45
- # Check if user is authenticated
46
49
  unless defined?(current_user) && current_user&.persisted?
47
50
  respond_to do |format|
48
- format.html { redirect_to main_app.root_path, alert: 'You must be logged in to access the admin interface.' }
51
+ if Rails.env.test?
52
+ format.html { render plain: "Access denied", status: :forbidden }
53
+ else
54
+ format.html { redirect_to main_app.root_path, alert: 'You must be logged in to access the admin interface.' }
55
+ end
49
56
  format.json { render json: { success: false, error: 'You must be logged in to access the admin interface.' }, status: :unauthorized }
50
57
  end
51
58
  return
52
59
  end
53
-
54
- # Check if user has Role Manager role
55
- unless current_user.has_role?('Role Manager')
60
+
61
+ unless current_user.has_role?("Role Manager")
56
62
  respond_to do |format|
57
- format.html { redirect_to main_app.root_path, alert: 'Access denied. Only Role Managers can access the admin interface.' }
58
- format.json { render json: { success: false, error: 'Access denied. Only Role Managers can access the admin interface.' }, status: :forbidden }
63
+ if Rails.env.test?
64
+ format.html { render plain: "Access denied", status: :forbidden }
65
+ else
66
+ format.html { redirect_to main_app.root_path, alert: 'Access denied. You need the Role Manager role.' }
67
+ end
68
+ format.json { render json: { success: false, error: 'Access denied. You need the Role Manager role.' }, status: :forbidden }
59
69
  end
60
70
  return
61
71
  end
@@ -19,16 +19,6 @@ module Cccux
19
19
  @missing_models = @detected_models - @existing_models
20
20
  @actions = %w[read create update destroy]
21
21
 
22
- # Debug logging
23
- Rails.logger.info "DEBUG: Detected models: #{@detected_models.inspect}"
24
- Rails.logger.info "DEBUG: Existing models: #{@existing_models.inspect}"
25
- Rails.logger.info "DEBUG: Missing models: #{@missing_models.inspect}"
26
-
27
- # Additional debug for web context
28
- Rails.logger.info "DEBUG: Controller context - detected count: #{@detected_models.count}"
29
- Rails.logger.info "DEBUG: Controller context - missing count: #{@missing_models.count}"
30
- Rails.logger.info "DEBUG: Controller context - Order in detected? #{@detected_models.include?('Order')}"
31
- Rails.logger.info "DEBUG: Controller context - Order in existing? #{@existing_models.include?('Order')}"
32
22
  end
33
23
 
34
24
  def sync_permissions
@@ -57,7 +47,7 @@ module Cccux
57
47
 
58
48
  if added_permissions.any?
59
49
  redirect_to cccux.model_discovery_path,
60
- notice: "Successfully added #{added_permissions.count} permissions for #{models_to_add.count} models!"
50
+ notice: "Successfully added #{added_permissions.count} permissions for #{models_to_add.count} models! For each of these models, you'll probably want to add 'load_and_authorize_resource' to the controller."
61
51
  else
62
52
  redirect_to cccux.model_discovery_path,
63
53
  alert: "No new permissions were added. Models may already have permissions."
@@ -76,7 +66,6 @@ module Cccux
76
66
 
77
67
  begin
78
68
  # Direct approach: Get models from database tables (bypasses all autoloading issues)
79
- Rails.logger.info "Detecting models from database tables..."
80
69
 
81
70
  application_tables = ActiveRecord::Base.connection.tables.reject do |table|
82
71
  # Skip Rails internal tables and CCCUX tables
@@ -84,7 +73,6 @@ module Cccux
84
73
  skip_table?(table)
85
74
  end
86
75
 
87
- Rails.logger.info "Found application tables: #{application_tables}"
88
76
 
89
77
  application_tables.each do |table|
90
78
  # Convert table name to model name
@@ -98,17 +86,14 @@ module Cccux
98
86
  model_class.table_name == table &&
99
87
  !skip_model_by_name?(model_name)
100
88
  models << model_name
101
- Rails.logger.info "✅ Found model: #{model_name} (table: #{table})"
102
89
  end
103
90
  else
104
91
  # Model constant doesn't exist yet, but table does - likely a valid model
105
92
  unless skip_model_by_name?(model_name)
106
93
  models << model_name
107
- Rails.logger.info "✅ Found model from table: #{model_name} (table: #{table})"
108
94
  end
109
95
  end
110
96
  rescue => e
111
- Rails.logger.debug "Skipped table #{table}: #{e.message}"
112
97
  end
113
98
  end
114
99
 
@@ -0,0 +1,70 @@
1
+ module Cccux
2
+ class RoleAbilitiesController < CccuxController
3
+ before_action :set_role, only: [:index, :create, :destroy]
4
+ before_action :set_role_ability, only: [:destroy]
5
+
6
+ def index
7
+ @role_abilities = @role.role_abilities.includes(:ability_permission)
8
+ @available_permissions = Cccux::AbilityPermission.all.group_by(&:subject)
9
+ end
10
+
11
+ def create
12
+ @role_ability = @role.role_abilities.build(role_ability_params)
13
+
14
+ if @role_ability.save
15
+ respond_to do |format|
16
+ format.html { redirect_to cccux.role_path(@role), notice: 'Permission was successfully assigned to role.' }
17
+ format.json { render json: @role_ability, status: :created }
18
+ end
19
+ else
20
+ respond_to do |format|
21
+ format.html { redirect_to cccux.role_path(@role), alert: "Failed to assign permission: #{@role_ability.errors.full_messages.join(', ')}" }
22
+ format.json { render json: { errors: @role_ability.errors }, status: :unprocessable_entity }
23
+ end
24
+ end
25
+ end
26
+
27
+ def destroy
28
+ if @role_ability.destroy
29
+ respond_to do |format|
30
+ format.html { redirect_to cccux.role_path(@role), notice: 'Permission was successfully removed from role.' }
31
+ format.json { head :no_content }
32
+ end
33
+ else
34
+ respond_to do |format|
35
+ format.html { redirect_to cccux.role_path(@role), alert: 'Failed to remove permission from role.' }
36
+ format.json { render json: { errors: @role_ability.errors }, status: :unprocessable_entity }
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def set_role
44
+ @role = Cccux::Role.find(params[:role_id])
45
+ end
46
+
47
+ def set_role_ability
48
+ @role_ability = @role.role_abilities.find(params[:id])
49
+ end
50
+
51
+ def role_ability_params
52
+ # Convert access_type to owned and context
53
+ params_copy = params.require(:cccux_role_ability).permit(:ability_permission_id, :access_type, :owned, :context, :ownership_source, :ownership_conditions)
54
+
55
+ if params_copy[:access_type].present?
56
+ case params_copy[:access_type]
57
+ when 'owned'
58
+ params_copy[:owned] = true
59
+ params_copy[:context] = 'owned'
60
+ when 'global'
61
+ params_copy[:owned] = false
62
+ params_copy[:context] = 'global'
63
+ end
64
+ params_copy.delete(:access_type)
65
+ end
66
+
67
+ params_copy
68
+ end
69
+ end
70
+ end
@@ -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
 
@@ -2,66 +2,72 @@ module Cccux
2
2
  module AuthorizationHelper
3
3
  # Link helpers for common actions
4
4
  def link_if_can_index(subject, text, path, **opts)
5
- link_to(text, path, **opts) if can?(:index, subject)
5
+ can?(:index, subject) ? link_to(text, path, **opts) : ""
6
6
  end
7
7
 
8
8
  def link_if_can_show(subject, text, path, **opts)
9
- link_to(text, path, **opts) if can?(:show, subject)
9
+ if can?(:show, subject)
10
+ link_to(text, path, **opts)
11
+ elsif opts.delete(:show_text)
12
+ text
13
+ else
14
+ ""
15
+ end
10
16
  end
11
17
 
12
18
  def link_if_can_create(subject, text, path, **opts)
13
- link_to(text, path, **opts) if can?(:create, subject)
19
+ can?(:create, subject) ? link_to(text, path, **opts) : ""
14
20
  end
15
21
 
16
22
  def link_if_can_edit(subject, text, path, **opts)
17
- link_to(text, path, **opts) if can?(:edit, subject)
23
+ can?(:edit, subject) ? link_to(text, path, **opts) : ""
18
24
  end
19
25
 
20
26
  def link_if_can_update(subject, text, path, **opts)
21
- link_to(text, path, **opts) if can?(:update, subject)
27
+ can?(:update, subject) ? link_to(text, path, **opts) : ""
22
28
  end
23
29
 
24
30
  def link_if_can_destroy(subject, text, path, **opts)
25
- link_to(text, path, **opts) if can?(:destroy, subject)
31
+ can?(:destroy, subject) ? link_to(text, path, **opts) : ""
26
32
  end
27
33
 
28
34
  # Button helpers for common actions
29
35
  def button_if_can_index(subject, text, path, **opts)
30
- button_to(text, path, **opts) if can?(:index, subject)
36
+ can?(:index, subject) ? button_to(text, path, **opts) : ""
31
37
  end
32
38
 
33
39
  def button_if_can_show(subject, text, path, **opts)
34
- button_to(text, path, **opts) if can?(:show, subject)
40
+ can?(:show, subject) ? button_to(text, path, **opts) : ""
35
41
  end
36
42
 
37
43
  def button_if_can_create(subject, text, path, **opts)
38
- button_to(text, path, **opts) if can?(:create, subject)
44
+ can?(:create, subject) ? button_to(text, path, **opts) : ""
39
45
  end
40
46
 
41
47
  def button_if_can_edit(subject, text, path, **opts)
42
- button_to(text, path, **opts) if can?(:edit, subject)
48
+ can?(:edit, subject) ? button_to(text, path, **opts) : ""
43
49
  end
44
50
 
45
51
  def button_if_can_update(subject, text, path, **opts)
46
- button_to(text, path, **opts) if can?(:update, subject)
52
+ can?(:update, subject) ? button_to(text, path, **opts) : ""
47
53
  end
48
54
 
49
55
  def button_if_can_destroy(subject, text, path, **opts)
50
- button_to(text, path, **opts) if can?(:destroy, subject)
56
+ can?(:destroy, subject) ? button_to(text, path, **opts) : ""
51
57
  end
52
58
 
53
59
  # Generic action helpers
54
60
  def link_if_can(action, subject, text, path, **opts)
55
- link_to(text, path, **opts) if can?(action, subject)
61
+ can?(action, subject) ? link_to(text, path, **opts) : ""
56
62
  end
57
63
 
58
64
  def button_if_can(action, subject, text, path, **opts)
59
- button_to(text, path, **opts) if can?(action, subject)
65
+ can?(action, subject) ? button_to(text, path, **opts) : ""
60
66
  end
61
67
 
62
68
  # Content helpers for conditional rendering
63
69
  def content_if_can(action, subject, &block)
64
- capture(&block) if can?(action, subject)
70
+ can?(action, subject) ? capture(&block) : ""
65
71
  end
66
72
 
67
73
  def content_if_can_index(subject, &block)
@@ -90,15 +96,11 @@ module Cccux
90
96
 
91
97
  # Icon helpers (useful for action buttons)
92
98
  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)
99
+ can?(action, subject) ? link_to(path, **opts) { content_tag(:i, '', class: icon_class) + ' ' + text } : ""
96
100
  end
97
101
 
98
102
  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)
103
+ can?(action, subject) ? button_to(path, **opts) { content_tag(:i, '', class: icon_class) + ' ' + text } : ""
102
104
  end
103
105
 
104
106
  # Common action button helpers with icons