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,317 @@
|
|
|
1
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
|
+
<h1>Edit Role: <%= @role.name %></h1>
|
|
3
|
+
<div>
|
|
4
|
+
<%= link_to "View Role", cccux.role_path(@role),
|
|
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", cccux.roles_path,
|
|
7
|
+
style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<%= form_with model: [@role], url: cccux.role_path(@role), local: true, method: :patch do |form| %>
|
|
12
|
+
<% if @role.errors.any? %>
|
|
13
|
+
<div style="background-color: #f8d7da; border: 1px solid #721c24; padding: 1rem; margin-bottom: 2rem; border-radius: 4px;">
|
|
14
|
+
<h4><%= pluralize(@role.errors.count, "error") %> prohibited this role from being saved:</h4>
|
|
15
|
+
<ul>
|
|
16
|
+
<% @role.errors.full_messages.each do |message| %>
|
|
17
|
+
<li><%= message %></li>
|
|
18
|
+
<% end %>
|
|
19
|
+
</ul>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem; margin-bottom: 2rem;">
|
|
24
|
+
<!-- Basic Role Information -->
|
|
25
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
26
|
+
<h3 style="margin-top: 0; color: #495057;">Basic Information</h3>
|
|
27
|
+
|
|
28
|
+
<div style="margin-bottom: 1.5rem;">
|
|
29
|
+
<%= form.label :name, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
30
|
+
<%= form.text_field :name,
|
|
31
|
+
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div style="margin-bottom: 1.5rem;">
|
|
35
|
+
<%= form.label :description, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
36
|
+
<%= form.text_area :description, rows: 4,
|
|
37
|
+
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem; resize: vertical;" %>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div style="margin-bottom: 1.5rem;">
|
|
41
|
+
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
42
|
+
<%= form.check_box :active, { checked: @role.active.nil? ? true : @role.active },
|
|
43
|
+
style: "margin-right: 0.5rem; transform: scale(1.2);" %>
|
|
44
|
+
<span style="font-weight: bold; color: #495057;">Active Role</span>
|
|
45
|
+
</label>
|
|
46
|
+
<small style="color: #6c757d; font-size: 0.9rem; margin-top: 0.25rem; display: block; margin-left: 1.5rem;">
|
|
47
|
+
Only active roles can be assigned to users. Inactive roles are hidden from user assignment forms.
|
|
48
|
+
</small>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div style="background-color: #f8f9fa; padding: 1rem; border-radius: 4px;">
|
|
52
|
+
<strong style="color: #495057;">Current Users:</strong>
|
|
53
|
+
<div style="margin-top: 0.5rem;">
|
|
54
|
+
<%= pluralize(@role.users.count, 'user') %> assigned to this role
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Permission Management -->
|
|
60
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
61
|
+
<h3 style="margin-top: 0; color: #495057;">Permissions</h3>
|
|
62
|
+
|
|
63
|
+
<!-- Permission Legend -->
|
|
64
|
+
<div style="background-color: #e7f3ff; border: 1px solid #b8daff; padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem;">
|
|
65
|
+
<h6 style="margin: 0 0 0.5rem 0; color: #004085; font-weight: bold;">Access Types:</h6>
|
|
66
|
+
<div style="font-size: 0.85rem; color: #004085; line-height: 1.4;">
|
|
67
|
+
<div style="margin-bottom: 0.25rem;"><strong>Global:</strong> Access all records everywhere</div>
|
|
68
|
+
<div style="margin-bottom: 0.25rem;"><strong>Owned:</strong> Access records you own, or records you have access to via a parent/manager relationship (e.g., store manager can access all orders in their store). Configure the ownership model and keys below to define what "owned" means for this permission.</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<% if @available_permissions.any? %>
|
|
73
|
+
<% @available_permissions.each do |subject, permissions| %>
|
|
74
|
+
<div style="margin-bottom: 1.5rem; padding: 1rem; border: 1px solid #e9ecef; border-radius: 4px;">
|
|
75
|
+
<h5 style="margin: 0 0 0.75rem 0; color: #495057; font-weight: bold;"><%= subject %></h5>
|
|
76
|
+
|
|
77
|
+
<!-- Action Permissions with Access Type Controls -->
|
|
78
|
+
<div style="display: grid; grid-template-columns: 1fr; gap: 0.5rem;">
|
|
79
|
+
<% permissions.each do |permission| %>
|
|
80
|
+
<%
|
|
81
|
+
# Check if this permission is currently assigned to the role
|
|
82
|
+
is_assigned = @role.ability_permissions.include?(permission)
|
|
83
|
+
|
|
84
|
+
# Get the current access type for this specific permission
|
|
85
|
+
role_ability = @role.role_abilities.find_by(ability_permission: permission)
|
|
86
|
+
current_access_type = role_ability&.access_type || 'global'
|
|
87
|
+
%>
|
|
88
|
+
|
|
89
|
+
<div style="display: flex; align-items: center; padding: 0.75rem; border: 1px solid #e9ecef; border-radius: 4px; background-color: #f8f9fa;"
|
|
90
|
+
data-permission-id="<%= permission.id %>">
|
|
91
|
+
|
|
92
|
+
<div style="margin-right: 1rem; min-width: 100px;">
|
|
93
|
+
<%= check_box_tag "role[ability_permission_ids][]",
|
|
94
|
+
permission.id,
|
|
95
|
+
is_assigned,
|
|
96
|
+
style: "margin-right: 0.5rem;",
|
|
97
|
+
class: "permission-checkbox",
|
|
98
|
+
data: { permission_id: permission.id } %>
|
|
99
|
+
<span style="font-size: 0.9rem; color: #495057; font-weight: bold;">
|
|
100
|
+
<%= permission.action.capitalize %>
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Access Type Controls -->
|
|
105
|
+
<div style="margin-left: auto; display: flex; align-items: center; gap: 1.5rem;">
|
|
106
|
+
<span style="font-size: 0.8rem; color: #6c757d; font-weight: bold;">Access:</span>
|
|
107
|
+
|
|
108
|
+
<%= radio_button_tag "role[permission_access_type][#{permission.id}]", "global",
|
|
109
|
+
current_access_type == 'global',
|
|
110
|
+
id: "access_global_#{permission.id}",
|
|
111
|
+
style: "margin-right: 0.25rem;",
|
|
112
|
+
class: "access-radio",
|
|
113
|
+
disabled: !is_assigned %>
|
|
114
|
+
<%= label_tag "access_global_#{permission.id}", "Global",
|
|
115
|
+
style: "font-size: 0.8rem; cursor: pointer; margin-right: 0.75rem;",
|
|
116
|
+
class: "access-label #{is_assigned ? '' : 'text-muted'}" %>
|
|
117
|
+
|
|
118
|
+
<%= radio_button_tag "role[permission_access_type][#{permission.id}]", "owned",
|
|
119
|
+
current_access_type == 'owned',
|
|
120
|
+
id: "access_owned_#{permission.id}",
|
|
121
|
+
style: "margin-right: 0.25rem;",
|
|
122
|
+
class: "access-radio",
|
|
123
|
+
disabled: !is_assigned %>
|
|
124
|
+
<%= label_tag "access_owned_#{permission.id}", "Owned",
|
|
125
|
+
style: "font-size: 0.8rem; cursor: pointer;",
|
|
126
|
+
class: "access-label #{is_assigned ? '' : 'text-muted'}" %>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Ownership Configuration (shown when "Owned" is selected) -->
|
|
131
|
+
<div class="ownership-config"
|
|
132
|
+
style="display: none; margin-top: 0.5rem; padding: 0.75rem; background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px;"
|
|
133
|
+
data-permission-id="<%= permission.id %>">
|
|
134
|
+
<div style="font-size: 0.8rem; color: #856404; font-weight: bold; margin-bottom: 0.5rem;">
|
|
135
|
+
Ownership Configuration:
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
139
|
+
<div>
|
|
140
|
+
<label style="font-size: 0.75rem; color: #856404; display: block; margin-bottom: 0.25rem;">
|
|
141
|
+
Ownership Model:
|
|
142
|
+
</label>
|
|
143
|
+
<%= select_tag "role[ownership_source][#{permission.id}]",
|
|
144
|
+
options_for_select([['Select model...', '']] + (@available_ownership_models || []).map { |m| [m, m] }, role_ability&.ownership_source),
|
|
145
|
+
class: "ownership-source-field",
|
|
146
|
+
data: { permission_id: permission.id },
|
|
147
|
+
style: "width: 100%; padding: 0.25rem; font-size: 0.75rem; border: 1px solid #ffeaa7; border-radius: 2px;" %>
|
|
148
|
+
<small style="font-size: 0.7rem; color: #856404;">
|
|
149
|
+
Model that defines ownership (optional)
|
|
150
|
+
</small>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div>
|
|
154
|
+
<label style="font-size: 0.75rem; color: #856404; display: block; margin-bottom: 0.25rem;">
|
|
155
|
+
Foreign Key:
|
|
156
|
+
</label>
|
|
157
|
+
<%= text_field_tag "role[ownership_foreign_key][#{permission.id}]",
|
|
158
|
+
(role_ability&.ownership_conditions.present? ? JSON.parse(role_ability.ownership_conditions)["foreign_key"] : nil),
|
|
159
|
+
placeholder: "e.g., store_id",
|
|
160
|
+
style: "width: 100%; padding: 0.25rem; font-size: 0.75rem; border: 1px solid #ffeaa7; border-radius: 2px;",
|
|
161
|
+
class: "ownership-foreign-key-field" %>
|
|
162
|
+
<small style="font-size: 0.7rem; color: #856404;">
|
|
163
|
+
Links to main model (auto-detected if blank)
|
|
164
|
+
</small>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div>
|
|
169
|
+
<label style="font-size: 0.75rem; color: #856404; display: block; margin-bottom: 0.25rem;">
|
|
170
|
+
User Key:
|
|
171
|
+
</label>
|
|
172
|
+
<%= text_field_tag "role[ownership_user_key][#{permission.id}]",
|
|
173
|
+
(role_ability&.ownership_conditions.present? ? JSON.parse(role_ability.ownership_conditions)["user_key"] : nil),
|
|
174
|
+
placeholder: "e.g., user_id",
|
|
175
|
+
style: "width: 100%; padding: 0.25rem; font-size: 0.75rem; border: 1px solid #ffeaa7; border-radius: 2px;",
|
|
176
|
+
class: "ownership-user-key-field" %>
|
|
177
|
+
<small style="font-size: 0.7rem; color: #856404;">
|
|
178
|
+
Identifies the user (defaults to user_id)
|
|
179
|
+
</small>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<% end %>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<% end %>
|
|
186
|
+
|
|
187
|
+
<!-- Hidden field to ensure empty array is sent when no permissions are selected -->
|
|
188
|
+
<%= hidden_field_tag "role[ability_permission_ids][]", "", id: nil %>
|
|
189
|
+
<% else %>
|
|
190
|
+
<p style="color: #6c757d; font-style: italic;">No permissions available to assign.</p>
|
|
191
|
+
<% end %>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<!-- Enhanced JavaScript for permission interactions -->
|
|
196
|
+
<script>
|
|
197
|
+
document.addEventListener('DOMContentLoaded', initializeRoleForm);
|
|
198
|
+
document.addEventListener('turbo:load', initializeRoleForm);
|
|
199
|
+
|
|
200
|
+
function initializeRoleForm() {
|
|
201
|
+
// Handle permission checkboxes and access type radios
|
|
202
|
+
const permissionCheckboxes = document.querySelectorAll('.permission-checkbox');
|
|
203
|
+
|
|
204
|
+
permissionCheckboxes.forEach(checkbox => {
|
|
205
|
+
const permissionId = checkbox.value;
|
|
206
|
+
const accessTypeRadios = document.querySelectorAll(`input[name="role[permission_access_type][${permissionId}]"]`);
|
|
207
|
+
const accessLabels = document.querySelectorAll(`label[for^="access_"][for$="_${permissionId}"]`);
|
|
208
|
+
const ownershipConfig = document.querySelector(`.ownership-config[data-permission-id="${permissionId}"]`);
|
|
209
|
+
|
|
210
|
+
function updateAccessTypeVisibility() {
|
|
211
|
+
const isChecked = checkbox.checked;
|
|
212
|
+
|
|
213
|
+
// Enable/disable access type radios
|
|
214
|
+
accessTypeRadios.forEach(radio => {
|
|
215
|
+
radio.disabled = !isChecked;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Update label opacity
|
|
219
|
+
accessLabels.forEach(label => {
|
|
220
|
+
label.style.opacity = isChecked ? '1' : '0.5';
|
|
221
|
+
if (isChecked) {
|
|
222
|
+
label.classList.remove('text-muted');
|
|
223
|
+
} else {
|
|
224
|
+
label.classList.add('text-muted');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Hide ownership config when permission is unchecked
|
|
229
|
+
if (ownershipConfig) {
|
|
230
|
+
if (!isChecked) {
|
|
231
|
+
ownershipConfig.style.display = 'none';
|
|
232
|
+
} else {
|
|
233
|
+
// Check if "Owned" is selected
|
|
234
|
+
const ownedRadio = document.querySelector(`#access_owned_${permissionId}`);
|
|
235
|
+
if (ownedRadio && ownedRadio.checked) {
|
|
236
|
+
ownershipConfig.style.display = 'block';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
checkbox.addEventListener('change', updateAccessTypeVisibility);
|
|
243
|
+
updateAccessTypeVisibility();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Handle access type radio changes to show/hide ownership config
|
|
247
|
+
const accessRadios = document.querySelectorAll('.access-radio');
|
|
248
|
+
|
|
249
|
+
accessRadios.forEach(radio => {
|
|
250
|
+
radio.addEventListener('change', function() {
|
|
251
|
+
// Extract permission ID from the radio button name or ID
|
|
252
|
+
const permissionId = this.name.match(/\[(\d+)\]$/)?.[1] || this.id.match(/_(\d+)$/)?.[1];
|
|
253
|
+
const ownershipConfig = document.querySelector(`.ownership-config[data-permission-id="${permissionId}"]`);
|
|
254
|
+
const checkbox = document.querySelector(`input[value="${permissionId}"].permission-checkbox`);
|
|
255
|
+
|
|
256
|
+
if (ownershipConfig && checkbox && checkbox.checked) {
|
|
257
|
+
if (this.value === 'owned') {
|
|
258
|
+
ownershipConfig.style.display = 'block';
|
|
259
|
+
} else {
|
|
260
|
+
ownershipConfig.style.display = 'none';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Handle ownership source model dropdown
|
|
267
|
+
const ownershipSourceSelects = document.querySelectorAll('.ownership-source-field');
|
|
268
|
+
|
|
269
|
+
ownershipSourceSelects.forEach(select => {
|
|
270
|
+
select.addEventListener('change', function() {
|
|
271
|
+
const permissionId = this.dataset.permissionId;
|
|
272
|
+
const selectedModel = this.value;
|
|
273
|
+
|
|
274
|
+
if (selectedModel) {
|
|
275
|
+
console.log(`Model selected for permission ${permissionId}: ${selectedModel}`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Add CSS for muted text
|
|
281
|
+
if (!document.getElementById('cccux-muted-style')) {
|
|
282
|
+
const style = document.createElement('style');
|
|
283
|
+
style.id = 'cccux-muted-style';
|
|
284
|
+
style.textContent = `
|
|
285
|
+
.text-muted {
|
|
286
|
+
color: #6c757d !important;
|
|
287
|
+
}
|
|
288
|
+
`;
|
|
289
|
+
document.head.appendChild(style);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<!-- Form Actions -->
|
|
295
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
296
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
297
|
+
<div>
|
|
298
|
+
<%= form.submit "Update Role",
|
|
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", cccux.role_path(@role),
|
|
301
|
+
style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<% unless @role.users.any? %>
|
|
305
|
+
<div>
|
|
306
|
+
<%= link_to "Delete Role", cccux.role_path(@role),
|
|
307
|
+
data: { "turbo-method": :delete, "turbo-confirm": "Are you sure? This action cannot be undone." },
|
|
308
|
+
style: "background-color: #dc3545; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 4px; font-size: 1rem;" %>
|
|
309
|
+
</div>
|
|
310
|
+
<% else %>
|
|
311
|
+
<div style="color: #6c757d; font-size: 0.9rem; font-style: italic;">
|
|
312
|
+
Cannot delete role with assigned users
|
|
313
|
+
</div>
|
|
314
|
+
<% end %>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
<% end %>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
|
+
<h1>Role Management</h1>
|
|
3
|
+
<%= link_to "New Role", cccux.new_role_path,
|
|
4
|
+
data: { turbo_frame: "new_role_form" },
|
|
5
|
+
style: "background-color: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Flash Messages Area -->
|
|
9
|
+
<div id="flash" style="margin-bottom: 1rem;">
|
|
10
|
+
<% if notice %>
|
|
11
|
+
<div class="alert alert-success" style="background-color: #d1edff; border: 1px solid #0c5460; padding: 1rem; border-radius: 4px;">
|
|
12
|
+
<%= notice %>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% if alert %>
|
|
16
|
+
<div class="alert alert-danger" style="background-color: #f8d7da; border: 1px solid #721c24; padding: 1rem; border-radius: 4px;">
|
|
17
|
+
<%= alert %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
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
|
+
<!-- Roles List -->
|
|
28
|
+
<div style="margin-bottom: 1rem;">
|
|
29
|
+
<h3 style="color: #495057; margin-bottom: 0.5rem;">Role Hierarchy</h3>
|
|
30
|
+
<p style="color: #6c757d; font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
31
|
+
<strong>Drag and drop</strong> roles to reorder their priority. Higher positions = higher priority.
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div id="roles_list"
|
|
36
|
+
data-reorder-url="<%= cccux.reorder_roles_path %>"
|
|
37
|
+
style="display: flex; flex-direction: column; gap: 1rem; max-width: 600px;">
|
|
38
|
+
<% @roles.each do |role| %>
|
|
39
|
+
<%= render "role", role: role %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<% if @roles.empty? %>
|
|
44
|
+
<div style="text-align: center; padding: 3rem; background-color: #f8f9fa; border-radius: 4px;">
|
|
45
|
+
<h3>No roles found</h3>
|
|
46
|
+
<p>Create your first role to get started.</p>
|
|
47
|
+
<%= link_to "Create Role", cccux.new_role_path,
|
|
48
|
+
data: { turbo_frame: "new_role_form" },
|
|
49
|
+
style: "background-color: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
50
|
+
</div>
|
|
51
|
+
<% end %>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
|
+
<h1>Role: <%= @role.name %></h1>
|
|
3
|
+
<div>
|
|
4
|
+
<%= link_to "Edit", cccux.edit_role_path(@role),
|
|
5
|
+
style: "background-color: #28a745; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px; margin-right: 0.5rem;" %>
|
|
6
|
+
<%= link_to "Back to Roles", cccux.roles_path,
|
|
7
|
+
style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
somewhere,.
|
|
11
|
+
|
|
12
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
|
13
|
+
<!-- Role Details -->
|
|
14
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
15
|
+
<h3 style="margin-top: 0; color: #495057;">Role Details</h3>
|
|
16
|
+
|
|
17
|
+
<div style="margin-bottom: 1rem;">
|
|
18
|
+
<strong style="color: #495057;">Name:</strong>
|
|
19
|
+
<span style="display: block; margin-top: 0.25rem; color: #6c757d;"><%= @role.name %></span>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div style="margin-bottom: 1rem;">
|
|
23
|
+
<strong style="color: #495057;">Description:</strong>
|
|
24
|
+
<span style="display: block; margin-top: 0.25rem; color: #6c757d;">
|
|
25
|
+
<%= @role.description.present? ? @role.description : "No description provided" %>
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div style="margin-bottom: 1rem;">
|
|
30
|
+
<strong style="color: #495057;">Users with this role:</strong>
|
|
31
|
+
<span style="display: block; margin-top: 0.25rem; color: #6c757d;">
|
|
32
|
+
<%= pluralize(@users_with_role.count, 'user') %>
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div>
|
|
37
|
+
<strong style="color: #495057;">Created:</strong>
|
|
38
|
+
<span style="display: block; margin-top: 0.25rem; color: #6c757d;">
|
|
39
|
+
<%= @role.created_at.strftime('%B %d, %Y at %I:%M %p') %>
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Users with this Role -->
|
|
45
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
46
|
+
<h3 style="margin-top: 0; color: #495057;">Users (<%= @users_with_role.count %>)</h3>
|
|
47
|
+
|
|
48
|
+
<% if @users_with_role.any? %>
|
|
49
|
+
<div style="max-height: 300px; overflow-y: auto;">
|
|
50
|
+
<% @users_with_role.each do |user| %>
|
|
51
|
+
<div style="padding: 0.75rem; border-bottom: 1px solid #e9ecef; display: flex; justify-content: space-between; align-items: center;">
|
|
52
|
+
<span style="color: #495057;"><%= user.email %></span>
|
|
53
|
+
<small style="color: #6c757d;"><%= user.created_at.strftime('%Y-%m-%d') %></small>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<% else %>
|
|
58
|
+
<p style="color: #6c757d; font-style: italic;">No users assigned to this role.</p>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Assigned Permissions -->
|
|
64
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem; margin-top: 2rem;">
|
|
65
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
|
66
|
+
<h3 style="margin: 0; color: #495057;">Assigned Permissions (<%= @role.ability_permissions.count %>)</h3>
|
|
67
|
+
<%= link_to "Manage Permissions", cccux.edit_role_path(@role),
|
|
68
|
+
style: "background-color: #007bff; color: white; padding: 0.375rem 0.75rem; text-decoration: none; border-radius: 4px; font-size: 0.875rem;" %>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<% if @role.ability_permissions.any? %>
|
|
72
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 0.75rem;">
|
|
73
|
+
<% @role.ability_permissions.each do |permission| %>
|
|
74
|
+
<div style="padding: 0.75rem; border: 1px solid #e9ecef; border-radius: 4px; background-color: #f8f9fa;">
|
|
75
|
+
<div style="font-weight: bold; color: #495057; margin-bottom: 0.25rem;">
|
|
76
|
+
<%= permission.subject %>
|
|
77
|
+
</div>
|
|
78
|
+
<div style="display: flex; align-items: center;">
|
|
79
|
+
<span style="background-color: <%= permission.action.in?(['read', 'create', 'update', 'destroy']) ? '#28a745' : '#007bff' %>;
|
|
80
|
+
color: white; padding: 0.125rem 0.375rem; border-radius: 12px; font-size: 0.75rem; font-weight: bold;">
|
|
81
|
+
<%= permission.action %>
|
|
82
|
+
</span>
|
|
83
|
+
<% if permission.description.present? %>
|
|
84
|
+
<span style="margin-left: 0.5rem; color: #6c757d; font-size: 0.875rem;">
|
|
85
|
+
<%= truncate(permission.description, length: 40) %>
|
|
86
|
+
</span>
|
|
87
|
+
<% end %>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<% end %>
|
|
91
|
+
</div>
|
|
92
|
+
<% else %>
|
|
93
|
+
<div style="text-align: center; padding: 2rem; color: #6c757d;">
|
|
94
|
+
<p style="margin-bottom: 1rem; font-style: italic;">No permissions assigned to this role.</p>
|
|
95
|
+
<%= link_to "Add Permissions", cccux.edit_role_path(@role),
|
|
96
|
+
style: "background-color: #28a745; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 4px; font-weight: bold;" %>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
|
2
|
+
<h1>Edit User: <%= @user.email %></h1>
|
|
3
|
+
<%= link_to "← Back to User", cccux.user_path(@user),
|
|
4
|
+
style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<%= form_with model: [@user], url: cccux.user_path(@user), method: :patch, local: true do |form| %>
|
|
8
|
+
<% if @user.errors.any? %>
|
|
9
|
+
<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;">
|
|
10
|
+
<h4 style="margin-top: 0;">Please fix the following errors:</h4>
|
|
11
|
+
<ul style="margin-bottom: 0;">
|
|
12
|
+
<% @user.errors.full_messages.each do |message| %>
|
|
13
|
+
<li><%= message %></li>
|
|
14
|
+
<% end %>
|
|
15
|
+
</ul>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-bottom: 2rem;">
|
|
20
|
+
<!-- User Information -->
|
|
21
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
22
|
+
<h3 style="margin-top: 0; color: #495057;">User Information</h3>
|
|
23
|
+
|
|
24
|
+
<div style="margin-bottom: 1.5rem;">
|
|
25
|
+
<%= form.label :email, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
26
|
+
<%= form.email_field :email,
|
|
27
|
+
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div style="margin-bottom: 1.5rem;">
|
|
31
|
+
<%= form.label :password, "New Password (leave blank to keep current)",
|
|
32
|
+
style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
33
|
+
<%= form.password_field :password,
|
|
34
|
+
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
35
|
+
<small style="color: #6c757d; font-size: 0.875rem;">Leave blank to keep current password</small>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div style="margin-bottom: 1.5rem;">
|
|
39
|
+
<%= form.label :password_confirmation, "Confirm New Password",
|
|
40
|
+
style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
|
|
41
|
+
<%= form.password_field :password_confirmation,
|
|
42
|
+
style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div style="background-color: #f8f9fa; padding: 1rem; border-radius: 4px;">
|
|
46
|
+
<strong style="color: #495057;">Account Info:</strong>
|
|
47
|
+
<div style="margin-top: 0.5rem; font-size: 0.9rem; color: #6c757d;">
|
|
48
|
+
Created: <%= @user.created_at.strftime('%b %d, %Y') %><br>
|
|
49
|
+
Last Updated: <%= @user.updated_at.strftime('%b %d, %Y') %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<!-- Role Management -->
|
|
55
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
56
|
+
<h3 style="margin-top: 0; color: #495057;">Role Assignment</h3>
|
|
57
|
+
|
|
58
|
+
<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
|
59
|
+
<p style="margin: 0; color: #856404; font-size: 0.9rem;">
|
|
60
|
+
<strong>Current:</strong> <%= @user.role_names.join(', ') %>
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<% if @available_roles.any? %>
|
|
65
|
+
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #e9ecef; border-radius: 4px; padding: 0.5rem;">
|
|
66
|
+
<% @available_roles.each do |role| %>
|
|
67
|
+
<div style="padding: 0.5rem; border-bottom: 1px solid #f8f9fa; display: flex; align-items: center;">
|
|
68
|
+
<%= check_box_tag "user[role_ids][]", role.id, @user_role_ids.include?(role.id),
|
|
69
|
+
id: "role_#{role.id}",
|
|
70
|
+
style: "margin-right: 0.5rem;" %>
|
|
71
|
+
<%= label_tag "role_#{role.id}", style: "margin: 0; cursor: pointer; flex: 1;" do %>
|
|
72
|
+
<strong style="color: #495057;"><%= role.name %></strong>
|
|
73
|
+
<% if role.description.present? %>
|
|
74
|
+
<br><small style="color: #6c757d;"><%= role.description %></small>
|
|
75
|
+
<% end %>
|
|
76
|
+
<% if @user_role_ids.include?(role.id) %>
|
|
77
|
+
<span style="background-color: #d4edda; color: #155724; padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.7rem; margin-left: 0.5rem;">CURRENT</span>
|
|
78
|
+
<% end %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div style="margin-top: 1rem; padding: 1rem; background-color: #e7f3ff; border-radius: 4px;">
|
|
85
|
+
<h5 style="margin: 0 0 0.5rem 0; color: #004085;">Role Selection Tips:</h5>
|
|
86
|
+
<ul style="margin: 0; padding-left: 1.5rem; color: #004085; font-size: 0.9rem;">
|
|
87
|
+
<li>Check roles to assign them to this user</li>
|
|
88
|
+
<li>Uncheck roles to remove them from this user</li>
|
|
89
|
+
<li>Users automatically keep their "Basic User" role</li>
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
<% else %>
|
|
93
|
+
<div style="text-align: center; padding: 2rem; color: #6c757d; font-style: italic;">
|
|
94
|
+
No roles available. Create roles first.
|
|
95
|
+
</div>
|
|
96
|
+
<% end %>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Form Actions -->
|
|
101
|
+
<div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
|
|
102
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
103
|
+
<div>
|
|
104
|
+
<%= form.submit "Update User",
|
|
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", cccux.user_path(@user),
|
|
107
|
+
style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div>
|
|
111
|
+
<%= link_to "Delete User", cccux.user_path(@user),
|
|
112
|
+
data: { "turbo-method": :delete, "turbo-confirm": "Are you sure? This will permanently delete the user and all their role assignments." },
|
|
113
|
+
style: "background-color: #dc3545; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 4px; font-size: 1rem;" %>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<% end %>
|