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