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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +124 -112
- data/Rakefile +57 -4
- data/app/assets/stylesheets/cccux/application.css +96 -72
- data/app/controllers/cccux/ability_permissions_controller.rb +138 -33
- data/app/controllers/cccux/application_controller.rb +7 -0
- data/app/controllers/cccux/cccux_controller.rb +20 -10
- data/app/controllers/cccux/dashboard_controller.rb +203 -32
- data/app/controllers/cccux/role_abilities_controller.rb +70 -0
- data/app/controllers/cccux/roles_controller.rb +70 -81
- data/app/controllers/cccux/users_controller.rb +22 -7
- data/app/controllers/concerns/cccux/application_controller_concern.rb +6 -2
- data/app/helpers/cccux/authorization_helper.rb +29 -21
- data/app/models/cccux/ability.rb +83 -32
- data/app/models/cccux/ability_permission.rb +9 -0
- data/app/models/cccux/post.rb +5 -0
- data/app/models/cccux/role.rb +19 -1
- data/app/models/cccux/role_ability.rb +3 -0
- data/app/models/concerns/cccux/user_concern.rb +5 -2
- data/app/views/cccux/ability_permissions/new.html.erb +2 -2
- data/app/views/cccux/dashboard/model_discovery.html.erb +7 -2
- data/app/views/cccux/roles/_form.html.erb +24 -71
- data/app/views/cccux/roles/edit.html.erb +5 -5
- data/app/views/cccux/roles/index.html.erb +1 -8
- data/app/views/cccux/roles/new.html.erb +1 -3
- data/app/views/cccux/users/edit.html.erb +4 -4
- data/app/views/cccux/users/new.html.erb +30 -15
- data/app/views/layouts/cccux/admin.html.erb +1 -2
- data/app/views/shared/_footer.html.erb +1 -1
- data/config/routes.rb +7 -6
- data/lib/cccux/engine.rb +7 -6
- data/lib/cccux/version.rb +1 -1
- data/lib/cccux.rb +12 -0
- data/lib/tasks/cccux.rake +271 -159
- metadata +10 -22
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module Cccux
|
|
2
2
|
class AbilityPermissionsController < CccuxController
|
|
3
|
-
# Ensure only Role Managers can access permission management
|
|
4
|
-
before_action :ensure_role_manager
|
|
5
3
|
|
|
6
4
|
before_action :set_ability_permission, only: [:show, :edit, :update, :destroy]
|
|
7
5
|
|
|
@@ -112,14 +110,10 @@ module Cccux
|
|
|
112
110
|
def create_single_permission
|
|
113
111
|
@ability_permission = Cccux::AbilityPermission.new(ability_permission_params)
|
|
114
112
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
else
|
|
118
|
-
@available_subjects = get_available_subjects
|
|
119
|
-
@available_actions = get_available_actions
|
|
120
|
-
@subject_actions_map = get_subject_actions_map
|
|
121
|
-
render :new
|
|
113
|
+
unless @ability_permission.save
|
|
114
|
+
raise "AbilityPermission creation failed: \n#{@ability_permission.errors.full_messages.join(', ')}"
|
|
122
115
|
end
|
|
116
|
+
redirect_to cccux.ability_permissions_path, notice: 'Permission was successfully created.'
|
|
123
117
|
end
|
|
124
118
|
|
|
125
119
|
def get_available_subjects
|
|
@@ -155,13 +149,26 @@ module Cccux
|
|
|
155
149
|
|
|
156
150
|
# Add route-discovered actions
|
|
157
151
|
route_actions = discover_actions_for_model(subject)
|
|
152
|
+
Rails.logger.debug "Route actions for #{subject}: #{route_actions}"
|
|
158
153
|
actions += route_actions
|
|
154
|
+
# Add module-specific custom actions
|
|
155
|
+
if subject.include?('::')
|
|
156
|
+
module_name = subject.split('::').first
|
|
157
|
+
model_name = subject.split('::').last
|
|
158
|
+
|
|
159
|
+
# Discover custom actions for this module's controllers
|
|
160
|
+
custom_actions = discover_custom_actions_for_module(module_name, model_name)
|
|
161
|
+
actions += custom_actions
|
|
162
|
+
Rails.logger.debug "Added #{module_name} custom actions: #{custom_actions}"
|
|
163
|
+
end
|
|
159
164
|
|
|
160
165
|
# Add existing actions for this subject
|
|
161
166
|
existing_actions = Cccux::AbilityPermission.where(subject: subject).distinct.pluck(:action).compact
|
|
162
167
|
actions += existing_actions
|
|
163
168
|
|
|
164
|
-
actions.uniq.sort
|
|
169
|
+
final_actions = actions.uniq.sort
|
|
170
|
+
Rails.logger.debug "Final actions for #{subject}: #{final_actions}"
|
|
171
|
+
final_actions
|
|
165
172
|
end
|
|
166
173
|
|
|
167
174
|
def discover_application_models
|
|
@@ -194,11 +201,73 @@ module Cccux
|
|
|
194
201
|
actions = []
|
|
195
202
|
|
|
196
203
|
begin
|
|
197
|
-
|
|
198
|
-
resource_name = subject.underscore.pluralize
|
|
204
|
+
Rails.logger.debug "Discovering actions for subject: #{subject}"
|
|
199
205
|
|
|
200
|
-
#
|
|
201
|
-
if subject.
|
|
206
|
+
# Handle namespaced models (e.g., MegaBar::Page)
|
|
207
|
+
if subject.include?('::')
|
|
208
|
+
module_name = subject.split('::').first
|
|
209
|
+
model_name = subject.split('::').last.underscore.pluralize
|
|
210
|
+
controller_pattern = "#{module_name.underscore}/#{model_name}"
|
|
211
|
+
|
|
212
|
+
Rails.logger.debug "Namespaced model detected. Looking for controller: #{controller_pattern}"
|
|
213
|
+
|
|
214
|
+
# Try to find the engine for this module
|
|
215
|
+
engine_class = "#{module_name}::Engine".constantize rescue nil
|
|
216
|
+
|
|
217
|
+
if engine_class
|
|
218
|
+
Rails.logger.debug "Found engine: #{engine_class}"
|
|
219
|
+
|
|
220
|
+
# Look through engine routes for this namespaced controller
|
|
221
|
+
engine_class.routes.routes.each do |route|
|
|
222
|
+
controller_name = route.defaults[:controller]
|
|
223
|
+
next unless controller_name&.start_with?("#{module_name.underscore}/")
|
|
224
|
+
next unless route.defaults[:action]
|
|
225
|
+
|
|
226
|
+
action = route.defaults[:action]
|
|
227
|
+
Rails.logger.debug "Found #{module_name} engine route: #{controller_name}##{action}"
|
|
228
|
+
|
|
229
|
+
# Map HTTP verbs to standard actions and include custom actions
|
|
230
|
+
case action
|
|
231
|
+
when 'index' then actions << 'read'
|
|
232
|
+
when 'show' then actions << 'read'
|
|
233
|
+
when 'create' then actions << 'create'
|
|
234
|
+
when 'update' then actions << 'update'
|
|
235
|
+
when 'destroy' then actions << 'destroy'
|
|
236
|
+
when 'edit', 'new' then next # Skip these as they're UI actions
|
|
237
|
+
else
|
|
238
|
+
# Custom actions like 'move', 'administer_page', etc.
|
|
239
|
+
actions << action
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
else
|
|
243
|
+
Rails.logger.debug "No engine found for #{module_name}, checking main routes"
|
|
244
|
+
|
|
245
|
+
# Fallback to main Rails routes
|
|
246
|
+
Rails.application.routes.routes.each do |route|
|
|
247
|
+
controller_name = route.defaults[:controller]
|
|
248
|
+
next unless controller_name&.start_with?("#{module_name.underscore}/")
|
|
249
|
+
next unless route.defaults[:action]
|
|
250
|
+
|
|
251
|
+
action = route.defaults[:action]
|
|
252
|
+
Rails.logger.debug "Found #{module_name} route: #{controller_name}##{action}"
|
|
253
|
+
|
|
254
|
+
# Map HTTP verbs to standard actions and include custom actions
|
|
255
|
+
case action
|
|
256
|
+
when 'index' then actions << 'read'
|
|
257
|
+
when 'show' then actions << 'read'
|
|
258
|
+
when 'create' then actions << 'create'
|
|
259
|
+
when 'update' then actions << 'update'
|
|
260
|
+
when 'destroy' then actions << 'destroy'
|
|
261
|
+
when 'edit', 'new' then next # Skip these as they're UI actions
|
|
262
|
+
else
|
|
263
|
+
# Custom actions like 'move', 'administer_page', etc.
|
|
264
|
+
actions << action
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Handle CCCUX models
|
|
270
|
+
elsif subject.start_with?('Cccux::')
|
|
202
271
|
cccux_resource_name = subject.gsub('Cccux::', '').underscore.pluralize
|
|
203
272
|
|
|
204
273
|
# Look through CCCUX engine routes for this resource
|
|
@@ -222,26 +291,31 @@ module Cccux
|
|
|
222
291
|
end
|
|
223
292
|
end
|
|
224
293
|
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Also look through main Rails routes for this resource
|
|
228
|
-
Rails.application.routes.routes.each do |route|
|
|
229
|
-
next unless route.path.spec.to_s.include?(resource_name)
|
|
230
294
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
#
|
|
244
|
-
|
|
295
|
+
# Handle regular models
|
|
296
|
+
else
|
|
297
|
+
resource_name = subject.underscore.pluralize
|
|
298
|
+
Rails.logger.debug "Regular model detected. Resource name: #{resource_name}"
|
|
299
|
+
|
|
300
|
+
# Look through main Rails routes for this resource
|
|
301
|
+
Rails.application.routes.routes.each do |route|
|
|
302
|
+
next unless route.path.spec.to_s.include?(resource_name)
|
|
303
|
+
|
|
304
|
+
# Extract action from route
|
|
305
|
+
if route.defaults[:action]
|
|
306
|
+
action = route.defaults[:action]
|
|
307
|
+
# Map HTTP verbs to standard actions and include custom actions
|
|
308
|
+
case action
|
|
309
|
+
when 'index' then actions << 'read'
|
|
310
|
+
when 'show' then actions << 'read'
|
|
311
|
+
when 'create' then actions << 'create'
|
|
312
|
+
when 'update' then actions << 'update'
|
|
313
|
+
when 'destroy' then actions << 'destroy'
|
|
314
|
+
when 'edit', 'new' then next # Skip these as they're UI actions
|
|
315
|
+
else
|
|
316
|
+
# Custom actions
|
|
317
|
+
actions << action
|
|
318
|
+
end
|
|
245
319
|
end
|
|
246
320
|
end
|
|
247
321
|
end
|
|
@@ -253,6 +327,37 @@ module Cccux
|
|
|
253
327
|
actions.uniq
|
|
254
328
|
end
|
|
255
329
|
|
|
330
|
+
def discover_custom_actions_for_module(module_name, model_name)
|
|
331
|
+
custom_actions = []
|
|
332
|
+
|
|
333
|
+
begin
|
|
334
|
+
# Convert model name to controller name pattern
|
|
335
|
+
controller_name = "#{module_name.downcase}/#{model_name.underscore.pluralize}"
|
|
336
|
+
|
|
337
|
+
Rails.logger.debug "Looking for custom actions in controller: #{controller_name}"
|
|
338
|
+
|
|
339
|
+
# Look through Rails routes for this module's controllers
|
|
340
|
+
Rails.application.routes.routes.each do |route|
|
|
341
|
+
next unless route.defaults[:controller]&.start_with?("#{module_name.downcase}/")
|
|
342
|
+
next unless route.defaults[:action]
|
|
343
|
+
|
|
344
|
+
action = route.defaults[:action]
|
|
345
|
+
|
|
346
|
+
# Skip standard CRUD actions and UI actions
|
|
347
|
+
next if %w[index show create update destroy new edit].include?(action)
|
|
348
|
+
|
|
349
|
+
# Include custom actions
|
|
350
|
+
custom_actions << action
|
|
351
|
+
Rails.logger.debug "Found custom action: #{action} in #{route.defaults[:controller]}"
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
rescue => e
|
|
355
|
+
Rails.logger.warn "Error discovering custom actions for #{module_name}::#{model_name}: #{e.message}"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
custom_actions.uniq
|
|
359
|
+
end
|
|
360
|
+
|
|
256
361
|
def skip_model?(model_class)
|
|
257
362
|
excluded_patterns = [
|
|
258
363
|
/^ActiveRecord::/,
|
|
@@ -29,7 +29,14 @@ module Cccux
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
+
def devise_controller?
|
|
33
|
+
# Only return true if Devise is available and this is a Devise controller
|
|
34
|
+
defined?(Devise) && respond_to?(:devise_controller?) ? super : false
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
def configure_permitted_parameters
|
|
38
|
+
return unless defined?(Devise) && respond_to?(:devise_parameter_sanitizer)
|
|
39
|
+
|
|
33
40
|
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name])
|
|
34
41
|
devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name])
|
|
35
42
|
end
|
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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.' }
|
|
55
|
+
end
|
|
56
|
+
format.json { render json: { success: false, error: 'You must be logged in.' }, status: :unauthorized }
|
|
50
57
|
end
|
|
51
58
|
return
|
|
52
59
|
end
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
unless current_user.has_role?('Role Manager')
|
|
60
|
+
|
|
61
|
+
unless current_user.has_role?("Role Manager")
|
|
56
62
|
respond_to do |format|
|
|
57
|
-
|
|
58
|
-
|
|
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,12 +47,18 @@ 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."
|
|
64
54
|
end
|
|
65
55
|
end
|
|
56
|
+
|
|
57
|
+
def clear_model_cache
|
|
58
|
+
clear_model_discovery_cache
|
|
59
|
+
redirect_to cccux.model_discovery_path,
|
|
60
|
+
notice: "Model discovery cache cleared! New models should now be visible."
|
|
61
|
+
end
|
|
66
62
|
|
|
67
63
|
# Handle any unmatched routes in CCCUX - redirect to home
|
|
68
64
|
def not_found
|
|
@@ -72,11 +68,63 @@ module Cccux
|
|
|
72
68
|
private
|
|
73
69
|
|
|
74
70
|
def detect_application_models
|
|
71
|
+
# Use cached results if available
|
|
72
|
+
return @detected_models_cache if @detected_models_cache
|
|
73
|
+
|
|
74
|
+
# Force Rails to reload constants for better model discovery
|
|
75
|
+
begin
|
|
76
|
+
Rails.application.eager_load! if Rails.application.config.eager_load
|
|
77
|
+
rescue => e
|
|
78
|
+
Rails.logger.warn "Could not eager load: #{e.message}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Also try to load models from app/models directory
|
|
82
|
+
begin
|
|
83
|
+
app_models_path = Rails.root.join('app', 'models')
|
|
84
|
+
if Dir.exist?(app_models_path)
|
|
85
|
+
Dir.glob(File.join(app_models_path, '**', '*.rb')).each do |model_file|
|
|
86
|
+
begin
|
|
87
|
+
load model_file unless $LOADED_FEATURES.include?(model_file)
|
|
88
|
+
rescue => e
|
|
89
|
+
Rails.logger.debug "Could not load model file #{model_file}: #{e.message}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
rescue => e
|
|
94
|
+
Rails.logger.warn "Could not load models from app/models: #{e.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
75
97
|
models = []
|
|
76
98
|
|
|
77
99
|
begin
|
|
78
|
-
#
|
|
79
|
-
Rails.logger.info "
|
|
100
|
+
# Primary approach: Use Rails' built-in model discovery
|
|
101
|
+
Rails.logger.info "🔍 Using Rails model discovery approach..."
|
|
102
|
+
|
|
103
|
+
# Get all ApplicationRecord descendants (this includes all models)
|
|
104
|
+
if defined?(ApplicationRecord)
|
|
105
|
+
ApplicationRecord.descendants.each do |model_class|
|
|
106
|
+
next if model_class.abstract_class?
|
|
107
|
+
next if model_class.name.nil?
|
|
108
|
+
next if skip_model_by_name?(model_class.name)
|
|
109
|
+
|
|
110
|
+
models << model_class.name
|
|
111
|
+
Rails.logger.debug "Found model via ApplicationRecord: #{model_class.name}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Fallback: Get all ActiveRecord::Base descendants
|
|
116
|
+
ActiveRecord::Base.descendants.each do |model_class|
|
|
117
|
+
next if model_class.abstract_class?
|
|
118
|
+
next if model_class.name.nil?
|
|
119
|
+
next if skip_model_by_name?(model_class.name)
|
|
120
|
+
next if models.include?(model_class.name) # Avoid duplicates
|
|
121
|
+
|
|
122
|
+
models << model_class.name
|
|
123
|
+
Rails.logger.debug "Found model via ActiveRecord::Base: #{model_class.name}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Secondary approach: Database table discovery for any missing models
|
|
127
|
+
Rails.logger.info "🔍 Using database table discovery as backup..."
|
|
80
128
|
|
|
81
129
|
application_tables = ActiveRecord::Base.connection.tables.reject do |table|
|
|
82
130
|
# Skip Rails internal tables and CCCUX tables
|
|
@@ -84,36 +132,46 @@ module Cccux
|
|
|
84
132
|
skip_table?(table)
|
|
85
133
|
end
|
|
86
134
|
|
|
87
|
-
|
|
135
|
+
# Discover modules and their table patterns
|
|
136
|
+
module_table_patterns = discover_module_table_patterns
|
|
88
137
|
|
|
89
138
|
application_tables.each do |table|
|
|
90
139
|
# Convert table name to model name
|
|
91
140
|
model_name = table.singularize.camelize
|
|
92
141
|
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
142
|
+
# Check if this table belongs to a discovered module
|
|
143
|
+
module_name = find_module_for_table(table, module_table_patterns)
|
|
144
|
+
if module_name
|
|
145
|
+
# Extract the model name from the table (e.g., 'pages' from 'mega_bar_pages')
|
|
146
|
+
# Use the module's table prefix to extract the model name
|
|
147
|
+
prefix = module_table_patterns[module_name]
|
|
148
|
+
model_part = table.gsub(prefix, '').singularize.camelize
|
|
149
|
+
model_name = "#{module_name}::#{model_part}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Only add if not already found and not skipped
|
|
153
|
+
unless models.include?(model_name) || skip_model_by_name?(model_name)
|
|
154
|
+
# Try to verify the model exists
|
|
155
|
+
begin
|
|
156
|
+
if Object.const_defined?(model_name)
|
|
157
|
+
model_class = Object.const_get(model_name)
|
|
158
|
+
if model_class.respond_to?(:table_name) && model_class.table_name == table
|
|
159
|
+
models << model_name
|
|
160
|
+
Rails.logger.debug "Found model via table discovery: #{model_name}"
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
# Model constant doesn't exist but table does - likely a valid model
|
|
106
164
|
models << model_name
|
|
107
|
-
Rails.logger.
|
|
165
|
+
Rails.logger.debug "Found potential model via table: #{model_name}"
|
|
108
166
|
end
|
|
167
|
+
rescue => e
|
|
168
|
+
Rails.logger.debug "Error verifying model #{model_name}: #{e.message}"
|
|
109
169
|
end
|
|
110
|
-
rescue => e
|
|
111
|
-
Rails.logger.debug "Skipped table #{table}: #{e.message}"
|
|
112
170
|
end
|
|
113
171
|
end
|
|
114
172
|
|
|
115
173
|
rescue => e
|
|
116
|
-
Rails.logger.error "Error detecting models
|
|
174
|
+
Rails.logger.error "Error detecting models: #{e.message}"
|
|
117
175
|
Rails.logger.error e.backtrace.join("\n")
|
|
118
176
|
end
|
|
119
177
|
|
|
@@ -127,7 +185,107 @@ module Cccux
|
|
|
127
185
|
Rails.logger.info " Application models: #{models.reject { |m| m.start_with?('Cccux::') }.join(', ')}"
|
|
128
186
|
Rails.logger.info " CCCUX models: #{models.select { |m| m.start_with?('Cccux::') }.join(', ')}"
|
|
129
187
|
|
|
130
|
-
|
|
188
|
+
# Cache the results
|
|
189
|
+
@detected_models_cache = models.uniq.sort
|
|
190
|
+
@detected_models_cache
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def discover_module_table_patterns
|
|
194
|
+
# Use cached results if available
|
|
195
|
+
return @module_table_patterns_cache if @module_table_patterns_cache
|
|
196
|
+
|
|
197
|
+
patterns = {}
|
|
198
|
+
|
|
199
|
+
# Skip Rails internal engines and third-party gems
|
|
200
|
+
skip_engines = %w[
|
|
201
|
+
Rails ActionView SolidCache Stimulus Turbo Importmap ActiveStorage
|
|
202
|
+
ActionCable ActionMailbox BestInPlace SolidCable SolidQueue ActionText
|
|
203
|
+
Devise Kaminari Mega132
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
# Find Rails engines by looking for Engine classes
|
|
207
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
208
|
+
next unless klass < Rails::Engine
|
|
209
|
+
next if klass == Rails::Engine # Skip the base class
|
|
210
|
+
|
|
211
|
+
# Extract module name from engine class (e.g., MegaBar::Engine -> MegaBar)
|
|
212
|
+
module_name = klass.name.split('::').first
|
|
213
|
+
next if skip_engines.include?(module_name) # Skip unwanted engines
|
|
214
|
+
|
|
215
|
+
# Convert module name to expected table prefix
|
|
216
|
+
table_prefix = module_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') + '_'
|
|
217
|
+
patterns[module_name] = table_prefix
|
|
218
|
+
Rails.logger.info "Discovered engine: #{module_name} with table prefix: #{table_prefix}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
Rails.logger.info "Module table patterns: #{patterns}"
|
|
222
|
+
|
|
223
|
+
# Cache the results
|
|
224
|
+
@module_table_patterns_cache = patterns
|
|
225
|
+
@module_table_patterns_cache
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def find_table_prefix_for_module(module_name)
|
|
229
|
+
begin
|
|
230
|
+
# Look for models in this module
|
|
231
|
+
module_const = Object.const_get(module_name)
|
|
232
|
+
|
|
233
|
+
# Find the first model in this module to determine the table prefix
|
|
234
|
+
module_const.constants.each do |const|
|
|
235
|
+
const_obj = module_const.const_get(const)
|
|
236
|
+
if const_obj.is_a?(Class) && const_obj < ActiveRecord::Base && const_obj != ActiveRecord::Base
|
|
237
|
+
table_name = const_obj.table_name
|
|
238
|
+
# Convert camelCase to snake_case for the prefix
|
|
239
|
+
expected_prefix = module_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') + '_'
|
|
240
|
+
if table_name.start_with?(expected_prefix)
|
|
241
|
+
Rails.logger.debug "Found table prefix for #{module_name}: #{expected_prefix}"
|
|
242
|
+
return expected_prefix
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
rescue => e
|
|
247
|
+
Rails.logger.debug "Could not find table prefix for module #{module_name}: #{e.message}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def find_table_prefix_for_engine(engine_class)
|
|
254
|
+
begin
|
|
255
|
+
# Get the module name from the engine class
|
|
256
|
+
module_name = engine_class.name.split('::').first
|
|
257
|
+
|
|
258
|
+
# Look for models in this module
|
|
259
|
+
module_const = Object.const_get(module_name)
|
|
260
|
+
|
|
261
|
+
# Find the first model in this module to determine the table prefix
|
|
262
|
+
module_const.constants.each do |const|
|
|
263
|
+
const_obj = module_const.const_get(const)
|
|
264
|
+
if const_obj.is_a?(Class) && const_obj < ActiveRecord::Base && const_obj != ActiveRecord::Base
|
|
265
|
+
table_name = const_obj.table_name
|
|
266
|
+
# Convert camelCase to snake_case for the prefix
|
|
267
|
+
expected_prefix = module_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') + '_'
|
|
268
|
+
if table_name.start_with?(expected_prefix)
|
|
269
|
+
Rails.logger.debug "Found table prefix for engine #{module_name}: #{expected_prefix}"
|
|
270
|
+
return expected_prefix
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
rescue => e
|
|
275
|
+
Rails.logger.debug "Could not find table prefix for engine #{engine_class.name}: #{e.message}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def find_module_for_table(table_name, module_patterns)
|
|
282
|
+
module_patterns.each do |module_name, prefix|
|
|
283
|
+
if table_name.start_with?(prefix)
|
|
284
|
+
return module_name
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
nil
|
|
131
289
|
end
|
|
132
290
|
|
|
133
291
|
def get_models_with_permissions
|
|
@@ -151,6 +309,9 @@ module Cccux
|
|
|
151
309
|
/Migration/
|
|
152
310
|
]
|
|
153
311
|
|
|
312
|
+
# Don't skip any namespaced models (they should be discoverable)
|
|
313
|
+
return false if model_name.include?('::')
|
|
314
|
+
|
|
154
315
|
excluded_patterns.any? { |pattern| model_name.match?(pattern) }
|
|
155
316
|
end
|
|
156
317
|
|
|
@@ -165,8 +326,18 @@ module Cccux
|
|
|
165
326
|
'versions' # PaperTrail
|
|
166
327
|
]
|
|
167
328
|
|
|
329
|
+
# Don't skip any module-prefixed tables (they should be discoverable)
|
|
330
|
+
# This will be handled by the module discovery logic
|
|
331
|
+
return false if table_name.include?('_') && !excluded_tables.include?(table_name)
|
|
332
|
+
|
|
168
333
|
excluded_tables.include?(table_name) ||
|
|
169
334
|
table_name.end_with?('_versions') # PaperTrail version tables
|
|
170
335
|
end
|
|
336
|
+
|
|
337
|
+
def clear_model_discovery_cache
|
|
338
|
+
@detected_models_cache = nil
|
|
339
|
+
@module_table_patterns_cache = nil
|
|
340
|
+
Rails.logger.info "🔄 Model discovery cache cleared"
|
|
341
|
+
end
|
|
171
342
|
end
|
|
172
343
|
end
|
|
@@ -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
|