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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +382 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/config/cccux_manifest.js +1 -0
  7. data/app/assets/stylesheets/cccux/application.css +102 -0
  8. data/app/controllers/cccux/ability_permissions_controller.rb +271 -0
  9. data/app/controllers/cccux/application_controller.rb +37 -0
  10. data/app/controllers/cccux/authorization_controller.rb +10 -0
  11. data/app/controllers/cccux/cccux_controller.rb +64 -0
  12. data/app/controllers/cccux/dashboard_controller.rb +172 -0
  13. data/app/controllers/cccux/home_controller.rb +19 -0
  14. data/app/controllers/cccux/roles_controller.rb +290 -0
  15. data/app/controllers/cccux/simple_controller.rb +7 -0
  16. data/app/controllers/cccux/users_controller.rb +112 -0
  17. data/app/controllers/concerns/cccux/application_controller_concern.rb +32 -0
  18. data/app/helpers/cccux/application_helper.rb +4 -0
  19. data/app/helpers/cccux/authorization_helper.rb +228 -0
  20. data/app/jobs/cccux/application_job.rb +4 -0
  21. data/app/mailers/cccux/application_mailer.rb +6 -0
  22. data/app/models/cccux/ability.rb +142 -0
  23. data/app/models/cccux/ability_permission.rb +61 -0
  24. data/app/models/cccux/application_record.rb +5 -0
  25. data/app/models/cccux/role.rb +90 -0
  26. data/app/models/cccux/role_ability.rb +49 -0
  27. data/app/models/cccux/user_role.rb +42 -0
  28. data/app/models/concerns/cccux/authorizable.rb +25 -0
  29. data/app/models/concerns/cccux/scoped_ownership.rb +183 -0
  30. data/app/models/concerns/cccux/user_concern.rb +87 -0
  31. data/app/views/cccux/ability_permissions/edit.html.erb +58 -0
  32. data/app/views/cccux/ability_permissions/index.html.erb +108 -0
  33. data/app/views/cccux/ability_permissions/new.html.erb +308 -0
  34. data/app/views/cccux/dashboard/index.html.erb +69 -0
  35. data/app/views/cccux/dashboard/model_discovery.html.erb +148 -0
  36. data/app/views/cccux/home/index.html.erb +42 -0
  37. data/app/views/cccux/roles/_flash.html.erb +10 -0
  38. data/app/views/cccux/roles/_form.html.erb +78 -0
  39. data/app/views/cccux/roles/_role.html.erb +67 -0
  40. data/app/views/cccux/roles/edit.html.erb +317 -0
  41. data/app/views/cccux/roles/index.html.erb +51 -0
  42. data/app/views/cccux/roles/new.html.erb +3 -0
  43. data/app/views/cccux/roles/show.html.erb +99 -0
  44. data/app/views/cccux/users/edit.html.erb +117 -0
  45. data/app/views/cccux/users/index.html.erb +99 -0
  46. data/app/views/cccux/users/new.html.erb +94 -0
  47. data/app/views/cccux/users/show.html.erb +138 -0
  48. data/app/views/layouts/cccux/admin.html.erb +168 -0
  49. data/app/views/layouts/cccux/application.html.erb +17 -0
  50. data/app/views/shared/_footer.html.erb +101 -0
  51. data/config/routes.rb +63 -0
  52. data/db/migrate/20250626194001_create_cccux_roles.rb +15 -0
  53. data/db/migrate/20250626194007_create_cccux_ability_permissions.rb +18 -0
  54. data/db/migrate/20250626194011_create_cccux_user_roles.rb +13 -0
  55. data/db/migrate/20250626194016_create_cccux_role_abilities.rb +10 -0
  56. data/db/migrate/20250627170611_add_owned_to_cccux_role_abilities.rb +9 -0
  57. data/db/migrate/20250705193709_add_context_to_cccux_role_abilities.rb +9 -0
  58. data/db/migrate/20250706214415_add_ownership_configuration_to_role_abilities.rb +21 -0
  59. data/db/seeds.rb +136 -0
  60. data/lib/cccux/engine.rb +50 -0
  61. data/lib/cccux/version.rb +3 -0
  62. data/lib/cccux.rb +7 -0
  63. data/lib/tasks/cccux.rake +703 -0
  64. data/lib/tasks/view_helpers.rake +274 -0
  65. metadata +188 -0
@@ -0,0 +1,58 @@
1
+ <div class="container mx-auto px-4 py-8">
2
+ <div class="max-w-2xl mx-auto">
3
+ <h1 class="text-3xl font-bold text-gray-900 mb-6">Edit Permission</h1>
4
+
5
+ <%= form_with(model: [@ability_permission], url: cccux.ability_permission_path(@ability_permission), local: true, method: :patch, class: "bg-white shadow-md rounded-lg p-6") do |form| %>
6
+ <% if @ability_permission.errors.any? %>
7
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
8
+ <strong><%= pluralize(@ability_permission.errors.count, "error") %> prohibited this permission from being saved:</strong>
9
+ <ul class="mt-2">
10
+ <% @ability_permission.errors.full_messages.each do |message| %>
11
+ <li><%= message %></li>
12
+ <% end %>
13
+ </ul>
14
+ </div>
15
+ <% end %>
16
+
17
+ <div class="mb-4">
18
+ <%= form.label :subject, class: "block text-sm font-medium text-gray-700 mb-2" %>
19
+ <%= form.select :subject,
20
+ options_for_select(@available_subjects.map { |s| [s, s] }, @ability_permission.subject),
21
+ { include_blank: false },
22
+ { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" } %>
23
+ <p class="mt-1 text-sm text-gray-500">The model/resource this permission applies to (e.g., Order, User)</p>
24
+ </div>
25
+
26
+ <div class="mb-4">
27
+ <%= form.label :action, class: "block text-sm font-medium text-gray-700 mb-2" %>
28
+ <%= form.select :action,
29
+ options_for_select(@available_actions.map { |a| [a, a] }, @ability_permission.action),
30
+ { include_blank: false },
31
+ { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" } %>
32
+ <p class="mt-1 text-sm text-gray-500">The action this permission allows (e.g., read, create, update, destroy)</p>
33
+ </div>
34
+
35
+ <div class="mb-6">
36
+ <%= form.label :description, class: "block text-sm font-medium text-gray-700 mb-2" %>
37
+ <%= form.text_area :description,
38
+ rows: 3,
39
+ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
40
+ placeholder: "Optional description of what this permission allows..." %>
41
+ <p class="mt-1 text-sm text-gray-500">Optional description to help explain what this permission does</p>
42
+ </div>
43
+
44
+ <div class="flex items-center justify-between">
45
+ <%= form.submit "Update Permission", class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" %>
46
+ <%= link_to "Cancel", cccux.ability_permissions_path, class: "text-gray-600 hover:text-gray-800" %>
47
+ </div>
48
+ <% end %>
49
+
50
+ <div class="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
51
+ <h3 class="text-sm font-medium text-yellow-800 mb-2">⚠️ Warning:</h3>
52
+ <p class="text-sm text-yellow-700">
53
+ This permission is currently used by <strong><%= @ability_permission.role_abilities.count %></strong> role(s).
54
+ Changing it will affect all roles that have this permission assigned.
55
+ </p>
56
+ </div>
57
+ </div>
58
+ </div>
@@ -0,0 +1,108 @@
1
+ <div class="container mx-auto px-4 py-8">
2
+ <div class="flex justify-between items-center mb-6">
3
+ <h1 class="text-3xl font-bold text-gray-900">Ability Permissions</h1>
4
+ <div class="text-sm text-gray-600">
5
+ Total: <%= @ability_permissions.count %> permissions across <%= @grouped_permissions.keys.count %> models
6
+ </div>
7
+ </div>
8
+
9
+ <% if @grouped_permissions.any? %>
10
+ <% @grouped_permissions.each do |subject, permissions| %>
11
+ <div class="bg-white shadow-md rounded-lg overflow-hidden mb-8">
12
+ <!-- Model Header -->
13
+ <div class="px-6 py-4 bg-gradient-to-r from-blue-500 to-blue-600 text-white">
14
+ <div class="flex justify-between items-center">
15
+ <div>
16
+ <h2 class="text-xl font-bold"><%= subject %> Permissions</h2>
17
+ <p class="text-blue-100 text-sm"><%= permissions.count %> permission(s) configured</p>
18
+ </div>
19
+ <%= link_to "Create New #{subject} Permission",
20
+ cccux.new_ability_permission_path(subject: subject),
21
+ class: "bg-white text-blue-600 hover:bg-blue-50 font-semibold py-2 px-4 rounded-lg transition-colors" %>
22
+ </div>
23
+ </div>
24
+
25
+ <!-- Permissions Grid -->
26
+ <div class="overflow-x-auto">
27
+ <table class="min-w-full divide-y divide-gray-200">
28
+ <thead class="bg-gray-50">
29
+ <tr>
30
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
31
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
32
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Used by Roles</th>
33
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
34
+ </tr>
35
+ </thead>
36
+ <tbody class="bg-white divide-y divide-gray-200">
37
+ <% permissions.each_with_index do |permission, index| %>
38
+ <tr class="<%= index.even? ? 'bg-white' : 'bg-gray-50' %> hover:bg-blue-50 transition-colors">
39
+ <td class="px-6 py-4 whitespace-nowrap">
40
+ <div class="flex items-center">
41
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
42
+ <%= permission.action %>
43
+ </span>
44
+ </div>
45
+ </td>
46
+ <td class="px-6 py-4">
47
+ <div class="text-sm text-gray-900">
48
+ <%= permission.description.present? ? permission.description : "No description provided" %>
49
+ </div>
50
+ </td>
51
+ <td class="px-6 py-4 whitespace-nowrap">
52
+ <% role_count = permission.role_abilities.count %>
53
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= role_count > 0 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
54
+ <%= role_count %> role<%= 's' if role_count != 1 %>
55
+ </span>
56
+ </td>
57
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
58
+ <div class="flex space-x-3">
59
+ <%= link_to "Edit", cccux.edit_ability_permission_path(permission),
60
+ class: "text-blue-600 hover:text-blue-900 font-medium" %>
61
+ <%= link_to "Delete", cccux.ability_permission_path(permission),
62
+ data: { "turbo-method": :delete, confirm: "Are you sure you want to delete this permission? This will affect #{permission.role_abilities.count} role(s)." },
63
+ class: "text-red-600 hover:text-red-900 font-medium" %>
64
+ </div>
65
+ </td>
66
+ </tr>
67
+ <% end %>
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+
72
+ <!-- Quick Stats Footer -->
73
+ <div class="px-6 py-3 bg-gray-50 border-t border-gray-200">
74
+ <div class="flex justify-between items-center text-sm text-gray-600">
75
+ <span>
76
+ Actions: <%= permissions.pluck(:action).uniq.join(', ') %>
77
+ </span>
78
+ <span>
79
+ Total roles using <%= subject %> permissions: <%= permissions.map(&:role_abilities).flatten.map(&:role_id).uniq.count %>
80
+ </span>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <% end %>
85
+ <% else %>
86
+ <!-- Empty State -->
87
+ <div class="bg-white shadow-md rounded-lg p-12 text-center">
88
+ <div class="mx-auto w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-4">
89
+ <svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
90
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"></path>
91
+ </svg>
92
+ </div>
93
+ <h3 class="text-lg font-medium text-gray-900 mb-2">No permissions found</h3>
94
+ <p class="text-gray-500 mb-6">Get started by creating your first permission for any model.</p>
95
+ <%= link_to "Create First Permission", cccux.new_ability_permission_path,
96
+ class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition-colors" %>
97
+ </div>
98
+ <% end %>
99
+
100
+ <!-- Navigation Footer -->
101
+ <div class="mt-8 flex justify-between items-center">
102
+ <%= link_to "← Back to Roles", cccux.roles_path,
103
+ class: "text-blue-600 hover:text-blue-800 font-medium" %>
104
+ <div class="text-sm text-gray-500">
105
+ Permissions are dynamically loaded into role management interfaces
106
+ </div>
107
+ </div>
108
+ </div>
@@ -0,0 +1,308 @@
1
+ <div class="container mx-auto px-4 py-8">
2
+ <div class="max-w-4xl mx-auto">
3
+ <div class="mb-6">
4
+ <h1 class="text-3xl font-bold text-gray-900 mb-2">Create New Permissions</h1>
5
+ <p class="text-gray-600">Create one or multiple permissions for a model with intelligent action discovery</p>
6
+ </div>
7
+
8
+ <%= form_with(model: [@ability_permission], url: cccux.ability_permissions_path, local: true, class: "bg-white shadow-lg rounded-lg p-6") do |form| %>
9
+ <% if @ability_permission.errors.any? %>
10
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
11
+ <strong><%= pluralize(@ability_permission.errors.count, "error") %> prohibited this permission from being saved:</strong>
12
+ <ul class="mt-2">
13
+ <% @ability_permission.errors.full_messages.each do |message| %>
14
+ <li><%= message %></li>
15
+ <% end %>
16
+ </ul>
17
+ </div>
18
+ <% end %>
19
+
20
+ <!-- Subject Selection -->
21
+ <div class="mb-6">
22
+ <%= form.label :subject, class: "block text-sm font-medium text-gray-700 mb-2" %>
23
+ <%= form.select :subject,
24
+ options_for_select([['Select a model...', '']] + @available_subjects.map { |s| [s, s] }, @ability_permission.subject),
25
+ { include_blank: false },
26
+ {
27
+ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm #{'bg-gray-100' if @ability_permission.subject.present?}",
28
+ readonly: @ability_permission.subject.present?,
29
+ id: 'subject-select'
30
+ } %>
31
+ <p class="mt-1 text-sm text-gray-500">
32
+ <% if @ability_permission.subject.present? %>
33
+ Creating permissions for <strong><%= @ability_permission.subject %></strong> model
34
+ <% else %>
35
+ Choose the model/resource for these permissions (e.g., Order, User, Product)
36
+ <% end %>
37
+ </p>
38
+ </div>
39
+
40
+ <!-- Actions Selection (Multi-select) -->
41
+ <div class="mb-6">
42
+ <label class="block text-sm font-medium text-gray-700 mb-2">Actions</label>
43
+ <div id="actions-container" class="border border-gray-300 rounded-md p-4 bg-gray-50 min-h-[100px]">
44
+ <div id="no-subject-message" class="text-gray-500 text-center py-8">
45
+ <i class="fas fa-arrow-up text-2xl mb-2"></i>
46
+ <p>Select a model above to see available actions</p>
47
+ </div>
48
+ <div id="actions-checkboxes" class="hidden grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
49
+ <!-- Actions will be populated here via JavaScript -->
50
+ </div>
51
+ </div>
52
+ <div class="mt-2 flex justify-between items-center">
53
+ <p class="text-sm text-gray-500">Select one or more actions to create permissions for</p>
54
+ <div class="space-x-2">
55
+ <button type="button" id="select-all-actions" class="text-sm text-blue-600 hover:text-blue-800">Select All</button>
56
+ <button type="button" id="select-crud-actions" class="text-sm text-green-600 hover:text-green-800">CRUD Only</button>
57
+ <button type="button" id="clear-actions" class="text-sm text-gray-600 hover:text-gray-800">Clear</button>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Description Template -->
63
+ <div class="mb-6">
64
+ <%= form.label :description, "Description Template", class: "block text-sm font-medium text-gray-700 mb-2" %>
65
+ <%= form.text_area :description,
66
+ rows: 2,
67
+ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
68
+ placeholder: "Leave blank for auto-generated descriptions...",
69
+ id: 'description-template' %>
70
+ <p class="mt-1 text-sm text-gray-500">If provided, will be used as a template. Otherwise, descriptions like "Read orders", "Create orders" will be auto-generated</p>
71
+ </div>
72
+
73
+ <!-- Active Status -->
74
+ <div class="mb-6">
75
+ <label class="flex items-center">
76
+ <%= form.check_box :active, { checked: true, class: "rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" } %>
77
+ <span class="ml-2 text-sm text-gray-700">Active (permissions will be immediately available for assignment)</span>
78
+ </label>
79
+ </div>
80
+
81
+ <!-- Preview Section -->
82
+ <div id="preview-section" class="mb-6 hidden">
83
+ <h3 class="text-lg font-medium text-gray-900 mb-3">Preview: Permissions to be created</h3>
84
+ <div id="preview-list" class="bg-blue-50 border border-blue-200 rounded-lg p-4 max-h-40 overflow-y-auto">
85
+ <!-- Preview items will be populated here -->
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Action Buttons -->
90
+ <div class="flex items-center justify-between">
91
+ <div>
92
+ <%= form.submit "Create Selected Permissions",
93
+ class: "bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-6 rounded focus:outline-none focus:shadow-outline",
94
+ id: 'submit-button',
95
+ disabled: true %>
96
+ <span id="permission-count" class="ml-3 text-sm text-gray-600"></span>
97
+ </div>
98
+ <%= link_to "Cancel", cccux.ability_permissions_path, class: "text-gray-600 hover:text-gray-800" %>
99
+ </div>
100
+ <% end %>
101
+
102
+ <!-- Help Section -->
103
+ <div class="mt-8 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-6">
104
+ <h3 class="text-lg font-medium text-blue-800 mb-3">🚀 Smart Permission Creation</h3>
105
+ <div class="grid md:grid-cols-2 gap-4 text-sm text-blue-700">
106
+ <div>
107
+ <h4 class="font-semibold mb-2">Auto-Discovery Features:</h4>
108
+ <ul class="space-y-1">
109
+ <li>• <strong>Route Analysis:</strong> Discovers actions from your app's routes</li>
110
+ <li>• <strong>Model Detection:</strong> Finds all models in your application</li>
111
+ <li>• <strong>Smart Suggestions:</strong> Shows relevant actions per model</li>
112
+ <li>• <strong>Bulk Creation:</strong> Create multiple permissions at once</li>
113
+ </ul>
114
+ </div>
115
+ <div>
116
+ <h4 class="font-semibold mb-2">Common Patterns:</h4>
117
+ <ul class="space-y-1">
118
+ <li>• <strong>CRUD:</strong> read, create, update, destroy</li>
119
+ <li>• <strong>Custom Actions:</strong> process_order, approve, cancel</li>
120
+ <li>• <strong>Admin Actions:</strong> manage (grants all permissions)</li>
121
+ <li>• <strong>View Only:</strong> read, index, show</li>
122
+ </ul>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <script>
130
+ document.addEventListener('DOMContentLoaded', function() {
131
+ const subjectSelect = document.getElementById('subject-select');
132
+ const actionsContainer = document.getElementById('actions-container');
133
+ const noSubjectMessage = document.getElementById('no-subject-message');
134
+ const actionsCheckboxes = document.getElementById('actions-checkboxes');
135
+ const previewSection = document.getElementById('preview-section');
136
+ const previewList = document.getElementById('preview-list');
137
+ const submitButton = document.getElementById('submit-button');
138
+ const permissionCount = document.getElementById('permission-count');
139
+
140
+ // Subject actions map from the controller
141
+ const subjectActionsMap = <%= raw @subject_actions_map.to_json %>;
142
+
143
+ // Handle subject selection change
144
+ subjectSelect.addEventListener('change', function() {
145
+ const selectedSubject = this.value;
146
+ console.log('Subject selected:', selectedSubject);
147
+
148
+ if (selectedSubject) {
149
+ loadActionsForSubject(selectedSubject);
150
+ } else {
151
+ showNoSubjectMessage();
152
+ }
153
+ });
154
+
155
+ // Load actions for a specific subject
156
+ function loadActionsForSubject(subject) {
157
+ const actions = subjectActionsMap[subject] || [];
158
+ console.log('Actions for', subject, ':', actions);
159
+
160
+ if (actions.length > 0) {
161
+ displayActions(actions, subject);
162
+ } else {
163
+ // Fallback to AJAX if not in map
164
+ fetchActionsFromServer(subject);
165
+ }
166
+ }
167
+
168
+ // Fetch actions from server via AJAX
169
+ function fetchActionsFromServer(subject) {
170
+ fetch(`<%= cccux.actions_for_subject_ability_permissions_path %>?subject=${encodeURIComponent(subject)}`)
171
+ .then(response => response.json())
172
+ .then(data => {
173
+ displayActions(data.actions, subject);
174
+ })
175
+ .catch(error => {
176
+ console.error('Error fetching actions:', error);
177
+ displayActions(['read', 'create', 'update', 'destroy'], subject);
178
+ });
179
+ }
180
+
181
+ // Display actions as checkboxes
182
+ function displayActions(actions, subject) {
183
+ noSubjectMessage.classList.add('hidden');
184
+ actionsCheckboxes.classList.remove('hidden');
185
+
186
+ actionsCheckboxes.innerHTML = '';
187
+
188
+ actions.forEach(action => {
189
+ const div = document.createElement('div');
190
+ div.className = 'flex items-center';
191
+
192
+ const checkbox = document.createElement('input');
193
+ checkbox.type = 'checkbox';
194
+ checkbox.name = 'ability_permission[actions][]';
195
+ checkbox.value = action;
196
+ checkbox.id = `action_${action}`;
197
+ checkbox.className = 'rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500';
198
+ checkbox.addEventListener('change', updatePreview);
199
+
200
+ const label = document.createElement('label');
201
+ label.htmlFor = `action_${action}`;
202
+ label.className = 'ml-2 text-sm text-gray-700 cursor-pointer flex-1';
203
+ label.textContent = action;
204
+
205
+ // Add action type badge
206
+ const badge = document.createElement('span');
207
+ badge.className = getActionBadgeClass(action);
208
+ badge.textContent = getActionType(action);
209
+
210
+ div.appendChild(checkbox);
211
+ div.appendChild(label);
212
+ div.appendChild(badge);
213
+ actionsCheckboxes.appendChild(div);
214
+ });
215
+
216
+ updatePreview();
217
+ }
218
+
219
+ // Show no subject selected message
220
+ function showNoSubjectMessage() {
221
+ noSubjectMessage.classList.remove('hidden');
222
+ actionsCheckboxes.classList.add('hidden');
223
+ previewSection.classList.add('hidden');
224
+ submitButton.disabled = true;
225
+ permissionCount.textContent = '';
226
+ }
227
+
228
+ // Update preview section
229
+ function updatePreview() {
230
+ const subject = subjectSelect.value;
231
+ const selectedActions = Array.from(document.querySelectorAll('input[name="ability_permission[actions][]"]:checked'))
232
+ .map(cb => cb.value);
233
+
234
+ if (selectedActions.length > 0 && subject) {
235
+ previewSection.classList.remove('hidden');
236
+ previewList.innerHTML = '';
237
+
238
+ selectedActions.forEach(action => {
239
+ const div = document.createElement('div');
240
+ div.className = 'flex items-center justify-between py-1';
241
+ div.innerHTML = `
242
+ <span class="font-medium">${action}</span>
243
+ <span class="text-sm text-gray-600">${subject}</span>
244
+ `;
245
+ previewList.appendChild(div);
246
+ });
247
+
248
+ submitButton.disabled = false;
249
+ permissionCount.textContent = `(${selectedActions.length} permission${selectedActions.length !== 1 ? 's' : ''})`;
250
+ } else {
251
+ previewSection.classList.add('hidden');
252
+ submitButton.disabled = true;
253
+ permissionCount.textContent = '';
254
+ }
255
+ }
256
+
257
+ // Quick selection buttons
258
+ document.getElementById('select-all-actions').addEventListener('click', function() {
259
+ document.querySelectorAll('input[name="ability_permission[actions][]"]').forEach(cb => {
260
+ cb.checked = true;
261
+ });
262
+ updatePreview();
263
+ });
264
+
265
+ document.getElementById('select-crud-actions').addEventListener('click', function() {
266
+ const crudActions = ['read', 'create', 'update', 'destroy'];
267
+ document.querySelectorAll('input[name="ability_permission[actions][]"]').forEach(cb => {
268
+ cb.checked = crudActions.includes(cb.value);
269
+ });
270
+ updatePreview();
271
+ });
272
+
273
+ document.getElementById('clear-actions').addEventListener('click', function() {
274
+ document.querySelectorAll('input[name="ability_permission[actions][]"]').forEach(cb => {
275
+ cb.checked = false;
276
+ });
277
+ updatePreview();
278
+ });
279
+
280
+ // Utility functions
281
+ function getActionBadgeClass(action) {
282
+ const crudActions = ['read', 'create', 'update', 'destroy'];
283
+ if (crudActions.includes(action)) {
284
+ return 'px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full';
285
+ } else if (action === 'manage') {
286
+ return 'px-2 py-1 text-xs bg-purple-100 text-purple-800 rounded-full';
287
+ } else {
288
+ return 'px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full';
289
+ }
290
+ }
291
+
292
+ function getActionType(action) {
293
+ const crudActions = ['read', 'create', 'update', 'destroy'];
294
+ if (crudActions.includes(action)) {
295
+ return 'CRUD';
296
+ } else if (action === 'manage') {
297
+ return 'ALL';
298
+ } else {
299
+ return 'CUSTOM';
300
+ }
301
+ }
302
+
303
+ // Initialize with current subject if set
304
+ if (subjectSelect.value) {
305
+ loadActionsForSubject(subjectSelect.value);
306
+ }
307
+ });
308
+ </script>
@@ -0,0 +1,69 @@
1
+ <h2>Authorization Dashboard</h2>
2
+
3
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 2rem 0;">
4
+ <div style="background: #e3f2fd; padding: 1rem; border-radius: 5px; text-align: center;">
5
+ <h3 style="margin: 0; color: #1976d2;">Users</h3>
6
+ <p style="font-size: 2rem; margin: 0.5rem 0; color: #1976d2;"><%= @user_count %></p>
7
+ <p style="margin: 0; color: #666;"><%= User.count %> total</p>
8
+ </div>
9
+
10
+ <div style="background: #e8f5e8; padding: 1rem; border-radius: 5px; text-align: center;">
11
+ <h3 style="margin: 0; color: #388e3c;">Roles</h3>
12
+ <p style="font-size: 2rem; margin: 0.5rem 0; color: #388e3c;"><%= @role_count %></p>
13
+ <p style="margin: 0; color: #666;"><%= Cccux::Role.where(active: true).count %> active</p>
14
+ </div>
15
+
16
+ <div style="background: #fff3e0; padding: 1rem; border-radius: 5px; text-align: center;">
17
+ <h3 style="margin: 0; color: #f57c00;">Permissions</h3>
18
+ <p style="font-size: 2rem; margin: 0.5rem 0; color: #f57c00;"><%= @permission_count %></p>
19
+ <p style="margin: 0; color: #666;"><%= Cccux::AbilityPermission.where(active: true).count %> active</p>
20
+ </div>
21
+
22
+ <div style="background: #f3e5f5; padding: 1rem; border-radius: 5px; text-align: center;">
23
+ <h3 style="margin: 0; color: #7b1fa2;">Assignments</h3>
24
+ <p style="font-size: 2rem; margin: 0.5rem 0; color: #7b1fa2;"><%= @total_assignments %></p>
25
+ <p style="margin: 0; color: #666;">user-role & role-permission</p>
26
+ </div>
27
+ </div>
28
+
29
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 2rem;">
30
+ <div>
31
+ <h3>Recent Users</h3>
32
+ <% recent_users = User.order(created_at: :desc).limit(5) %>
33
+ <% if recent_users.any? %>
34
+ <% recent_users.each do |user| %>
35
+ <div style="background: #f8f9fa; padding: 0.5rem; margin: 0.5rem 0; border-radius: 3px;">
36
+ <strong><%= user.email %></strong>
37
+ <span style="color: #666; font-size: 0.9em;">- <%= user.created_at.strftime("%b %d, %Y") %></span>
38
+ </div>
39
+ <% end %>
40
+ <% else %>
41
+ <p style="color: #666;">No users yet</p>
42
+ <% end %>
43
+ </div>
44
+
45
+ <div>
46
+ <h3>Recent Roles</h3>
47
+ <% recent_roles = Cccux::Role.order(created_at: :desc).limit(5) %>
48
+ <% if recent_roles.any? %>
49
+ <% recent_roles.each do |role| %>
50
+ <div style="background: #f8f9fa; padding: 0.5rem; margin: 0.5rem 0; border-radius: 3px;">
51
+ <strong><%= role.name %></strong>
52
+ <span style="color: #666; font-size: 0.9em;">- <%= role.created_at.strftime("%b %d, %Y") %></span>
53
+ </div>
54
+ <% end %>
55
+ <% else %>
56
+ <p style="color: #666;">No roles yet</p>
57
+ <% end %>
58
+ </div>
59
+ </div>
60
+
61
+ <div style="margin-top: 2rem; padding: 1rem; background: #f0f8ff; border-radius: 5px;">
62
+ <h3>Quick Actions</h3>
63
+ <p>
64
+ <%= link_to "Create New User", cccux.new_user_path, style: "background: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 3px; margin-right: 1rem;" %>
65
+ <%= link_to "Create New Role", cccux.new_role_path, style: "background: #28a745; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 3px; margin-right: 1rem;" %>
66
+ <%= link_to "Manage Permissions", cccux.ability_permissions_path, style: "background: #ffc107; color: #212529; padding: 0.5rem 1rem; text-decoration: none; border-radius: 3px; margin-right: 1rem;" %>
67
+ <%= link_to "🔍 Model Discovery", cccux.model_discovery_path, style: "background: #17a2b8; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 3px; font-weight: bold;" %>
68
+ </p>
69
+ </div>