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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +124 -112
- data/Rakefile +57 -4
- data/app/assets/stylesheets/cccux/application.css +39 -84
- data/app/controllers/cccux/ability_permissions_controller.rb +3 -9
- data/app/controllers/cccux/application_controller.rb +7 -0
- data/app/controllers/cccux/cccux_controller.rb +19 -9
- data/app/controllers/cccux/dashboard_controller.rb +1 -16
- 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 +23 -21
- data/app/models/cccux/ability.rb +82 -32
- 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 +2 -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/config/routes.rb +6 -6
- data/lib/cccux/version.rb +1 -1
- data/lib/tasks/cccux.rake +23 -2
- metadata +10 -22
data/app/models/cccux/ability.rb
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Cccux
|
|
4
4
|
class Ability
|
|
5
|
-
|
|
5
|
+
include CanCan::Ability
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
def initialize(user, context = nil)
|
|
8
8
|
user ||= User.new # guest user (not logged in)
|
|
9
9
|
@context = context || {}
|
|
10
10
|
|
|
@@ -60,24 +60,33 @@ module Cccux
|
|
|
60
60
|
def apply_access_ability(role_ability, permission, model_class, user)
|
|
61
61
|
action = permission.action.to_sym
|
|
62
62
|
|
|
63
|
+
# Handle action aliases (read includes show and index)
|
|
64
|
+
actions_to_grant = case action
|
|
65
|
+
when :read
|
|
66
|
+
[:read, :show, :index]
|
|
67
|
+
when :update
|
|
68
|
+
[:update, :edit]
|
|
69
|
+
when :create
|
|
70
|
+
[:create, :new]
|
|
71
|
+
when :destroy
|
|
72
|
+
[:destroy, :delete]
|
|
73
|
+
else
|
|
74
|
+
[action]
|
|
75
|
+
end
|
|
76
|
+
|
|
63
77
|
# For User resource, keep owned logic for now
|
|
64
78
|
if permission.subject == 'User'
|
|
65
|
-
if role_ability.context == 'owned' ||
|
|
66
|
-
apply_owned_ability(
|
|
79
|
+
if role_ability.context == 'owned' || role_ability.owned
|
|
80
|
+
actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
|
|
67
81
|
else
|
|
68
|
-
can
|
|
82
|
+
actions_to_grant.each { |act| can act, model_class }
|
|
69
83
|
end
|
|
70
84
|
else
|
|
71
|
-
# For all other resources, use global/owned
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
can action, model_class
|
|
75
|
-
when 'owned'
|
|
76
|
-
apply_owned_ability(action, model_class, user, role_ability)
|
|
85
|
+
# For all other resources, use global/owned logic based on context field
|
|
86
|
+
if role_ability.context == 'owned' || role_ability.owned
|
|
87
|
+
actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
|
|
77
88
|
else
|
|
78
|
-
|
|
79
|
-
Rails.logger.warn "CCCUX: Unknown access_type '#{role_ability.access_type}' for #{model_class.name}, denying access"
|
|
80
|
-
# Don't grant any permissions - CanCanCan denies by default
|
|
89
|
+
actions_to_grant.each { |act| can act, model_class }
|
|
81
90
|
end
|
|
82
91
|
end
|
|
83
92
|
end
|
|
@@ -89,13 +98,31 @@ module Cccux
|
|
|
89
98
|
if ownership_model && user&.persisted?
|
|
90
99
|
# Parse conditions (should be a JSON string or nil)
|
|
91
100
|
conditions = role_ability.ownership_conditions.present? ? JSON.parse(role_ability.ownership_conditions) : {}
|
|
92
|
-
foreign_key = conditions["foreign_key"]
|
|
101
|
+
foreign_key = conditions["foreign_key"]
|
|
93
102
|
user_key = conditions["user_key"] || "user_id"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
|
|
104
|
+
# Require foreign_key to be explicitly specified when using ownership model
|
|
105
|
+
if foreign_key.present?
|
|
106
|
+
# Find all records owned by user via the join model
|
|
107
|
+
owned_ids = ownership_model.where(user_key => user.id).pluck(foreign_key)
|
|
108
|
+
if owned_ids.any?
|
|
109
|
+
# Check if the target model has the foreign key column
|
|
110
|
+
if model_class.column_names.include?(foreign_key)
|
|
111
|
+
# Direct ownership: model has the foreign key (e.g., Comment has post_id)
|
|
112
|
+
can action, model_class, foreign_key.to_sym => owned_ids
|
|
113
|
+
else
|
|
114
|
+
# Indirect ownership: model doesn't have the foreign key, use primary key
|
|
115
|
+
# This means the foreign key in the join table refers to the target model's primary key
|
|
116
|
+
can action, model_class, id: owned_ids
|
|
117
|
+
end
|
|
118
|
+
else
|
|
119
|
+
can action, model_class, id: []
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
# Fall back to no access if foreign_key is not specified
|
|
123
|
+
can action, model_class, id: []
|
|
124
|
+
end
|
|
97
125
|
else
|
|
98
|
-
Rails.logger.warn "CCCUX: Invalid ownership_source #{role_ability.ownership_source} for #{model_class.name}"
|
|
99
126
|
can action, model_class, id: []
|
|
100
127
|
end
|
|
101
128
|
# 2. Model custom owned_by?
|
|
@@ -112,30 +139,53 @@ module Cccux
|
|
|
112
139
|
else
|
|
113
140
|
can action, model_class, scoped_records
|
|
114
141
|
end
|
|
115
|
-
# 4.
|
|
142
|
+
# 4. Special case for User model (self-ownership)
|
|
143
|
+
elsif model_class == User
|
|
144
|
+
can action, model_class, id: user.id
|
|
145
|
+
# 5. Standard user_id
|
|
116
146
|
elsif model_class.column_names.include?('user_id')
|
|
117
147
|
can action, model_class, user_id: user.id
|
|
118
|
-
#
|
|
148
|
+
# 6. Standard creator_id
|
|
119
149
|
elsif model_class.column_names.include?('creator_id')
|
|
120
150
|
can action, model_class, creator_id: user.id
|
|
151
|
+
# 7. Dynamic ownership check for individual records
|
|
121
152
|
else
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
153
|
+
# For cases where we need to check individual record attributes
|
|
154
|
+
can action, model_class do |record|
|
|
155
|
+
if record.respond_to?(:creator_id)
|
|
156
|
+
record.creator_id == user.id
|
|
157
|
+
elsif record.respond_to?(:user_id)
|
|
158
|
+
record.user_id == user.id
|
|
159
|
+
else
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
end
|
|
125
163
|
end
|
|
126
164
|
end
|
|
127
165
|
|
|
128
166
|
def resolve_model_class(subject)
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
# Try to resolve the model class in a robust way
|
|
168
|
+
candidates = []
|
|
169
|
+
if subject.include?("::")
|
|
170
|
+
candidates << subject
|
|
171
|
+
candidates << subject.split("::").last
|
|
132
172
|
else
|
|
133
|
-
|
|
134
|
-
|
|
173
|
+
candidates << subject
|
|
174
|
+
candidates << "Cccux::#{subject}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Add more candidates for common patterns
|
|
178
|
+
candidates << subject.split("::").last if subject.include?("::")
|
|
179
|
+
candidates << subject.gsub("Cccux::", "") if subject.start_with?("Cccux::")
|
|
180
|
+
|
|
181
|
+
candidates.each do |candidate|
|
|
182
|
+
begin
|
|
183
|
+
klass = candidate.constantize
|
|
184
|
+
return klass
|
|
185
|
+
rescue NameError => e
|
|
186
|
+
next
|
|
187
|
+
end
|
|
135
188
|
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
189
|
nil
|
|
140
190
|
end
|
|
141
191
|
end
|
data/app/models/cccux/role.rb
CHANGED
|
@@ -9,10 +9,11 @@ module Cccux
|
|
|
9
9
|
has_many :role_abilities, dependent: :destroy, class_name: 'Cccux::RoleAbility'
|
|
10
10
|
has_many :ability_permissions, through: :role_abilities, class_name: 'Cccux::AbilityPermission'
|
|
11
11
|
|
|
12
|
-
validates :name, presence: true
|
|
12
|
+
validates :name, presence: true
|
|
13
13
|
validates :priority, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
|
14
14
|
|
|
15
15
|
after_initialize :set_default_priority, if: :new_record?
|
|
16
|
+
validate :name_uniqueness_case_insensitive
|
|
16
17
|
|
|
17
18
|
scope :active, -> { where(active: true) }
|
|
18
19
|
scope :ordered, -> { order(:priority, :name) }
|
|
@@ -81,6 +82,23 @@ module Cccux
|
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
# Generate slug from name
|
|
86
|
+
def slug
|
|
87
|
+
name.parameterize.underscore if name.present?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Case insensitive name validation
|
|
91
|
+
def name_uniqueness_case_insensitive
|
|
92
|
+
return unless name.present?
|
|
93
|
+
|
|
94
|
+
existing_role = self.class.where('LOWER(name) = ?', name.downcase)
|
|
95
|
+
existing_role = existing_role.where.not(id: id) if persisted?
|
|
96
|
+
|
|
97
|
+
if existing_role.exists?
|
|
98
|
+
errors.add(:name, 'has already been taken')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
84
102
|
private
|
|
85
103
|
|
|
86
104
|
def set_default_priority
|
|
@@ -5,6 +5,9 @@ module Cccux
|
|
|
5
5
|
belongs_to :role, class_name: 'Cccux::Role'
|
|
6
6
|
belongs_to :ability_permission, class_name: 'Cccux::AbilityPermission'
|
|
7
7
|
|
|
8
|
+
# Delegate methods to ability_permission
|
|
9
|
+
delegate :action, :subject, :active?, to: :ability_permission, allow_nil: true
|
|
10
|
+
|
|
8
11
|
# Simplified access types: global, owned
|
|
9
12
|
ACCESS_TYPES = %w[global owned].freeze
|
|
10
13
|
|
|
@@ -15,7 +15,7 @@ module Cccux
|
|
|
15
15
|
|
|
16
16
|
# Instance methods for user authorization
|
|
17
17
|
def has_role?(role_name)
|
|
18
|
-
|
|
18
|
+
cccux_user_roles.active.joins(:role).where(cccux_roles: { name: role_name }).exists?
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def has_any_role?(*role_names)
|
|
@@ -59,7 +59,7 @@ module Cccux
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def role_names
|
|
62
|
-
|
|
62
|
+
cccux_user_roles.active.joins(:role).pluck('cccux_roles.name')
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def highest_priority_role
|
|
@@ -1,78 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<div
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
<%= form_with(model: role, local: true) do |form| %>
|
|
2
|
+
<% if role.errors.any? %>
|
|
3
|
+
<div id="error_explanation">
|
|
4
|
+
<h2><%= pluralize(role.errors.count, "error") %> prohibited this role from being saved:</h2>
|
|
5
|
+
<ul>
|
|
6
|
+
<% role.errors.each do |error| %>
|
|
7
|
+
<li><%= error.full_message %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
8
10
|
</div>
|
|
9
|
-
|
|
10
|
-
<%= form_with model: [role], url: role.new_record? ? cccux.roles_path : cccux.role_path(role), local: false do |form| %>
|
|
11
|
-
<% if role.errors.any? %>
|
|
12
|
-
<div style="background-color: #f8d7da; border: 1px solid #721c24; padding: 1rem; margin-bottom: 1rem; border-radius: 4px;">
|
|
13
|
-
<h4 style="margin-top: 0; color: #721c24;"><%= pluralize(role.errors.count, "error") %> prohibited this role from being saved:</h4>
|
|
14
|
-
<ul style="margin-bottom: 0; color: #721c24;">
|
|
15
|
-
<% role.errors.full_messages.each do |message| %>
|
|
16
|
-
<li><%= message %></li>
|
|
17
|
-
<% end %>
|
|
18
|
-
</ul>
|
|
19
|
-
</div>
|
|
20
|
-
<% end %>
|
|
11
|
+
<% end %>
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
placeholder: "Enter role name (e.g., Manager, Support, etc.)" %>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div style="margin-bottom: 1.5rem;">
|
|
30
|
-
<%= form.label :description, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
31
|
-
<%= form.text_area :description, rows: 3,
|
|
32
|
-
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem; resize: vertical;",
|
|
33
|
-
placeholder: "Describe what this role can do and its responsibilities..." %>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<div style="margin-bottom: 1.5rem;">
|
|
37
|
-
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
38
|
-
<%= form.check_box :active, { checked: role.active.nil? ? true : role.active },
|
|
39
|
-
style: "margin-right: 0.5rem; transform: scale(1.2);" %>
|
|
40
|
-
<span style="font-weight: bold; color: #495057;">Active Role</span>
|
|
41
|
-
</label>
|
|
42
|
-
<small style="color: #6c757d; font-size: 0.9rem; margin-top: 0.25rem; display: block; margin-left: 1.5rem;">
|
|
43
|
-
Only active roles can be assigned to users. Inactive roles are hidden from user assignment forms.
|
|
44
|
-
</small>
|
|
45
|
-
</div>
|
|
13
|
+
<div class="field">
|
|
14
|
+
<%= form.label :name %>
|
|
15
|
+
<%= form.text_field :name %>
|
|
16
|
+
</div>
|
|
46
17
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
placeholder: "Enter priority (lower numbers = higher priority)",
|
|
52
|
-
min: 1,
|
|
53
|
-
value: role.priority || 50 %>
|
|
54
|
-
<small style="color: #6c757d; font-size: 0.9rem; margin-top: 0.25rem; display: block;">
|
|
55
|
-
Priority determines role hierarchy. Lower numbers = higher priority (e.g., Admin=1, Manager=10, User=50)
|
|
56
|
-
</small>
|
|
57
|
-
</div>
|
|
18
|
+
<div class="field">
|
|
19
|
+
<%= form.label :description %>
|
|
20
|
+
<%= form.text_area :description %>
|
|
21
|
+
</div>
|
|
58
22
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
After creating this role, you'll be able to assign specific permissions (read, create, update, destroy)
|
|
64
|
-
for each resource (Orders, Users, etc.) on the role's edit page.
|
|
65
|
-
</p>
|
|
66
|
-
</div>
|
|
67
|
-
<% end %>
|
|
23
|
+
<div class="field">
|
|
24
|
+
<%= form.label :priority %>
|
|
25
|
+
<%= form.number_field :priority %>
|
|
26
|
+
</div>
|
|
68
27
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
style: "background-color: #007bff; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;" %>
|
|
72
|
-
<%= link_to "Cancel", cccux.roles_path,
|
|
73
|
-
data: { turbo_frame: role.new_record? ? "new_role_form" : "role_#{role.id}" },
|
|
74
|
-
style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
|
|
75
|
-
</div>
|
|
76
|
-
<% end %>
|
|
28
|
+
<div class="actions">
|
|
29
|
+
<%= form.submit %>
|
|
77
30
|
</div>
|
|
78
31
|
<% end %>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
2
|
<h1>Edit Role: <%= @role.name %></h1>
|
|
3
3
|
<div>
|
|
4
|
-
<%= link_to "View Role",
|
|
4
|
+
<%= link_to "View Role", role_path(@role),
|
|
5
5
|
style: "background-color: #17a2b8; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px; margin-right: 0.5rem;" %>
|
|
6
|
-
<%= link_to "Back to Roles",
|
|
6
|
+
<%= link_to "Back to Roles", roles_path,
|
|
7
7
|
style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
8
8
|
</div>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
<%= form_with model: [@role], url:
|
|
11
|
+
<%= form_with model: [@role], url: role_path(@role), local: true, method: :patch do |form| %>
|
|
12
12
|
<% if @role.errors.any? %>
|
|
13
13
|
<div style="background-color: #f8d7da; border: 1px solid #721c24; padding: 1rem; margin-bottom: 2rem; border-radius: 4px;">
|
|
14
14
|
<h4><%= pluralize(@role.errors.count, "error") %> prohibited this role from being saved:</h4>
|
|
@@ -297,13 +297,13 @@
|
|
|
297
297
|
<div>
|
|
298
298
|
<%= form.submit "Update Role",
|
|
299
299
|
style: "background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-right: 1rem;" %>
|
|
300
|
-
<%= link_to "Cancel",
|
|
300
|
+
<%= link_to "Cancel", role_path(@role),
|
|
301
301
|
style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
|
|
302
302
|
</div>
|
|
303
303
|
|
|
304
304
|
<% unless @role.users.any? %>
|
|
305
305
|
<div>
|
|
306
|
-
<%= link_to "Delete Role",
|
|
306
|
+
<%= link_to "Delete Role", role_path(@role),
|
|
307
307
|
data: { "turbo-method": :delete, "turbo-confirm": "Are you sure? This action cannot be undone." },
|
|
308
308
|
style: "background-color: #dc3545; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 4px; font-size: 1rem;" %>
|
|
309
309
|
</div>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
2
|
<h1>Role Management</h1>
|
|
3
3
|
<%= link_to "New Role", cccux.new_role_path,
|
|
4
|
-
data: { turbo_frame: "new_role_form" },
|
|
5
4
|
style: "background-color: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
6
5
|
</div>
|
|
7
6
|
|
|
@@ -19,16 +18,11 @@
|
|
|
19
18
|
<% end %>
|
|
20
19
|
</div>
|
|
21
20
|
|
|
22
|
-
<!-- New Role Form Area -->
|
|
23
|
-
<%= turbo_frame_tag "new_role_form" do %>
|
|
24
|
-
<!-- Form will load here when "New Role" is clicked -->
|
|
25
|
-
<% end %>
|
|
26
|
-
|
|
27
21
|
<!-- Roles List -->
|
|
28
22
|
<div style="margin-bottom: 1rem;">
|
|
29
23
|
<h3 style="color: #495057; margin-bottom: 0.5rem;">Role Hierarchy</h3>
|
|
30
24
|
<p style="color: #6c757d; font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
31
|
-
|
|
25
|
+
Roles are listed by priority. Higher positions = higher priority.
|
|
32
26
|
</p>
|
|
33
27
|
</div>
|
|
34
28
|
|
|
@@ -45,7 +39,6 @@
|
|
|
45
39
|
<h3>No roles found</h3>
|
|
46
40
|
<p>Create your first role to get started.</p>
|
|
47
41
|
<%= link_to "Create Role", cccux.new_role_path,
|
|
48
|
-
data: { turbo_frame: "new_role_form" },
|
|
49
42
|
style: "background-color: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
50
43
|
</div>
|
|
51
44
|
<% end %>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
2
|
<h1>Edit User: <%= @user.email %></h1>
|
|
3
|
-
<%= link_to "โ Back to User",
|
|
3
|
+
<%= link_to "โ Back to User", user_path(@user),
|
|
4
4
|
style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
<%= form_with model: [@user], url:
|
|
7
|
+
<%= form_with model: [@user], url: user_path(@user), method: :patch, local: true do |form| %>
|
|
8
8
|
<% if @user.errors.any? %>
|
|
9
9
|
<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;">
|
|
10
10
|
<h4 style="margin-top: 0;">Please fix the following errors:</h4>
|
|
@@ -103,12 +103,12 @@
|
|
|
103
103
|
<div>
|
|
104
104
|
<%= form.submit "Update User",
|
|
105
105
|
style: "background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-right: 1rem;" %>
|
|
106
|
-
<%= link_to "Cancel",
|
|
106
|
+
<%= link_to "Cancel", user_path(@user),
|
|
107
107
|
style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
|
|
108
108
|
</div>
|
|
109
109
|
|
|
110
110
|
<div>
|
|
111
|
-
<%= link_to "Delete User",
|
|
111
|
+
<%= link_to "Delete User", user_path(@user),
|
|
112
112
|
data: { "turbo-method": :delete, "turbo-confirm": "Are you sure? This will permanently delete the user and all their role assignments." },
|
|
113
113
|
style: "background-color: #dc3545; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 4px; font-size: 1rem;" %>
|
|
114
114
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
2
|
<h1>Create New User</h1>
|
|
3
|
-
<%= link_to "โ Back to Users",
|
|
3
|
+
<%= link_to "โ Back to Users", users_path,
|
|
4
4
|
style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
<%= form_with model: [@user], url:
|
|
7
|
+
<%= form_with model: [@user], url: users_path, local: true do |form| %>
|
|
8
8
|
<% if @user.errors.any? %>
|
|
9
9
|
<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;">
|
|
10
10
|
<h4 style="margin-top: 0;">Please fix the following errors:</h4>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</div>
|
|
17
17
|
<% end %>
|
|
18
18
|
|
|
19
|
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;
|
|
19
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
|
20
20
|
<!-- User Information -->
|
|
21
21
|
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
22
22
|
<h3 style="margin-top: 0; color: #495057;">User Information</h3>
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
<%= form.label :password, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
32
32
|
<%= form.password_field :password,
|
|
33
33
|
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
34
|
-
<small style="color: #6c757d; font-size: 0.875rem;">Minimum 6 characters</small>
|
|
35
34
|
</div>
|
|
36
35
|
|
|
37
36
|
<div style="margin-bottom: 1.5rem;">
|
|
@@ -39,21 +38,32 @@
|
|
|
39
38
|
<%= form.password_field :password_confirmation,
|
|
40
39
|
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
41
40
|
</div>
|
|
41
|
+
|
|
42
|
+
<div style="margin-bottom: 1.5rem;">
|
|
43
|
+
<%= form.label :first_name, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
44
|
+
<%= form.text_field :first_name,
|
|
45
|
+
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div style="margin-bottom: 1.5rem;">
|
|
49
|
+
<%= form.label :last_name, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
50
|
+
<%= form.text_field :last_name,
|
|
51
|
+
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
52
|
+
</div>
|
|
42
53
|
</div>
|
|
43
54
|
|
|
44
55
|
<!-- Role Assignment -->
|
|
45
56
|
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
46
57
|
<h3 style="margin-top: 0; color: #495057;">Role Assignment</h3>
|
|
47
58
|
|
|
48
|
-
<div style="background-color: #
|
|
49
|
-
<p style="margin: 0; color: #
|
|
50
|
-
<strong>Note:</strong> Users automatically receive the "Basic User" role
|
|
51
|
-
You can assign additional roles here.
|
|
59
|
+
<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
|
60
|
+
<p style="margin: 0; color: #856404; font-size: 0.9rem;">
|
|
61
|
+
<strong>Note:</strong> Users will automatically receive the "Basic User" role.
|
|
52
62
|
</p>
|
|
53
63
|
</div>
|
|
54
64
|
|
|
55
65
|
<% if @roles.any? %>
|
|
56
|
-
<div style="max-height:
|
|
66
|
+
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #e9ecef; border-radius: 4px; padding: 0.5rem;">
|
|
57
67
|
<% @roles.each do |role| %>
|
|
58
68
|
<div style="padding: 0.5rem; border-bottom: 1px solid #f8f9fa; display: flex; align-items: center;">
|
|
59
69
|
<%= check_box_tag "user[role_ids][]", role.id, false,
|
|
@@ -68,6 +78,15 @@
|
|
|
68
78
|
</div>
|
|
69
79
|
<% end %>
|
|
70
80
|
</div>
|
|
81
|
+
|
|
82
|
+
<div style="margin-top: 1rem; padding: 1rem; background-color: #e7f3ff; border-radius: 4px;">
|
|
83
|
+
<h5 style="margin: 0 0 0.5rem 0; color: #004085;">Role Selection Tips:</h5>
|
|
84
|
+
<ul style="margin: 0; padding-left: 1.5rem; color: #004085; font-size: 0.9rem;">
|
|
85
|
+
<li>Check roles to assign them to this user</li>
|
|
86
|
+
<li>Users automatically keep their "Basic User" role</li>
|
|
87
|
+
<li>Role assignments can be modified later</li>
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
71
90
|
<% else %>
|
|
72
91
|
<div style="text-align: center; padding: 2rem; color: #6c757d; font-style: italic;">
|
|
73
92
|
No roles available. Create roles first.
|
|
@@ -77,18 +96,14 @@
|
|
|
77
96
|
</div>
|
|
78
97
|
|
|
79
98
|
<!-- Form Actions -->
|
|
80
|
-
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
99
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem; margin-top: 2rem;">
|
|
81
100
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
82
101
|
<div>
|
|
83
102
|
<%= form.submit "Create User",
|
|
84
103
|
style: "background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-right: 1rem;" %>
|
|
85
|
-
<%= link_to "Cancel",
|
|
104
|
+
<%= link_to "Cancel", users_path,
|
|
86
105
|
style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
|
|
87
106
|
</div>
|
|
88
|
-
|
|
89
|
-
<div style="color: #6c757d; font-size: 0.9rem;">
|
|
90
|
-
<em>User will receive login credentials and can change password after first login</em>
|
|
91
|
-
</div>
|
|
92
107
|
</div>
|
|
93
108
|
</div>
|
|
94
109
|
<% end %>
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
<%= csp_meta_tag %>
|
|
8
8
|
|
|
9
9
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
10
|
-
|
|
11
|
-
<%= javascript_importmap_tags %>
|
|
10
|
+
<%# Remove JavaScript for now to avoid asset pipeline issues in tests %>
|
|
12
11
|
|
|
13
12
|
<!-- Sortable.js for drag and drop -->
|
|
14
13
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
data/config/routes.rb
CHANGED
|
@@ -2,8 +2,11 @@ Cccux::Engine.routes.draw do
|
|
|
2
2
|
# Devise authentication routes - use different path to avoid conflicts
|
|
3
3
|
# devise_for :users, class_name: 'Cccux::User', path: 'auth'
|
|
4
4
|
|
|
5
|
+
# Root route for the engine - goes to dashboard
|
|
5
6
|
root 'dashboard#index'
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
# Dashboard route (alias for root)
|
|
9
|
+
get '/dashboard', to: 'dashboard#index', as: :dashboard
|
|
7
10
|
|
|
8
11
|
# Model Discovery Routes
|
|
9
12
|
get 'model-discovery', to: 'dashboard#model_discovery', as: :model_discovery
|
|
@@ -22,6 +25,7 @@ Cccux::Engine.routes.draw do
|
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
resources :roles do
|
|
28
|
+
resources :role_abilities, only: [:index, :create, :destroy]
|
|
25
29
|
member do
|
|
26
30
|
patch :toggle_active
|
|
27
31
|
get :permissions
|
|
@@ -52,11 +56,7 @@ Cccux::Engine.routes.draw do
|
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
collection do
|
|
57
|
-
get :search
|
|
58
|
-
end
|
|
59
|
-
end
|
|
59
|
+
|
|
60
60
|
|
|
61
61
|
# Catch-all route for any unmatched paths in CCCUX - redirect to home
|
|
62
62
|
match '*unmatched', to: 'dashboard#not_found', via: :all
|
data/lib/cccux/version.rb
CHANGED
data/lib/tasks/cccux.rake
CHANGED
|
@@ -59,7 +59,15 @@ namespace :cccux do
|
|
|
59
59
|
|
|
60
60
|
# Step 4: Run CCCUX migrations
|
|
61
61
|
puts "๐ Step 4: Running CCCUX migrations..."
|
|
62
|
-
|
|
62
|
+
begin
|
|
63
|
+
Rake::Task['db:migrate'].invoke
|
|
64
|
+
rescue RuntimeError => e
|
|
65
|
+
if e.message.include?("Don't know how to build task 'db:migrate'")
|
|
66
|
+
puts " โ ๏ธ Skipping migrations (not available in engine context)"
|
|
67
|
+
else
|
|
68
|
+
raise e
|
|
69
|
+
end
|
|
70
|
+
end
|
|
63
71
|
puts "โ
CCCUX migrations completed"
|
|
64
72
|
|
|
65
73
|
# Step 5: Include CCCUX concern in User model
|
|
@@ -106,6 +114,19 @@ namespace :cccux do
|
|
|
106
114
|
puts "๐ Need help? Check the CCCUX documentation or README"
|
|
107
115
|
end
|
|
108
116
|
|
|
117
|
+
desc 'test:prepare - Prepare test database for CCCUX engine'
|
|
118
|
+
task 'test:prepare' => :environment do
|
|
119
|
+
puts "๐งช Preparing CCCUX test database..."
|
|
120
|
+
|
|
121
|
+
# Switch to test environment
|
|
122
|
+
Rails.env = 'test'
|
|
123
|
+
|
|
124
|
+
# Load schema into test database
|
|
125
|
+
system("cd #{Rails.root.join('..', '..')} && RAILS_ENV=test rails db:schema:load")
|
|
126
|
+
|
|
127
|
+
puts "โ
Test database prepared"
|
|
128
|
+
end
|
|
129
|
+
|
|
109
130
|
desc 'test - Test CCCUX + Devise integration'
|
|
110
131
|
task test: :environment do
|
|
111
132
|
puts "๐งช Testing CCCUX + Devise integration..."
|
|
@@ -661,7 +682,7 @@ namespace :cccux do
|
|
|
661
682
|
layout_content = File.read(layout_path)
|
|
662
683
|
|
|
663
684
|
# Check if footer is already included
|
|
664
|
-
unless layout_content.include?(
|
|
685
|
+
unless layout_content.include?("render 'shared/footer'")
|
|
665
686
|
# Add footer before closing body tag
|
|
666
687
|
if layout_content.include?('</body>')
|
|
667
688
|
updated_content = layout_content.gsub(
|