qdadm 0.35.0 → 0.38.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 (43) hide show
  1. package/README.md +27 -174
  2. package/package.json +2 -1
  3. package/src/auth/SessionAuthAdapter.js +114 -3
  4. package/src/components/editors/PermissionEditor.vue +535 -0
  5. package/src/components/forms/FormField.vue +1 -11
  6. package/src/components/index.js +1 -0
  7. package/src/components/layout/AppLayout.vue +20 -8
  8. package/src/components/layout/defaults/DefaultToaster.vue +3 -3
  9. package/src/components/pages/LoginPage.vue +26 -5
  10. package/src/composables/useCurrentEntity.js +26 -17
  11. package/src/composables/useForm.js +7 -0
  12. package/src/composables/useFormPageBuilder.js +7 -0
  13. package/src/composables/useNavContext.js +30 -16
  14. package/src/core/index.js +0 -3
  15. package/src/debug/AuthCollector.js +199 -31
  16. package/src/debug/Collector.js +24 -2
  17. package/src/debug/EntitiesCollector.js +8 -0
  18. package/src/debug/SignalCollector.js +60 -2
  19. package/src/debug/components/panels/AuthPanel.vue +159 -27
  20. package/src/debug/components/panels/EntitiesPanel.vue +18 -2
  21. package/src/entity/EntityManager.js +205 -36
  22. package/src/entity/auth/EntityAuthAdapter.js +54 -46
  23. package/src/entity/auth/SecurityChecker.js +110 -42
  24. package/src/entity/auth/factory.js +11 -2
  25. package/src/entity/auth/factory.test.js +29 -0
  26. package/src/entity/storage/factory.test.js +6 -5
  27. package/src/index.js +3 -0
  28. package/src/kernel/Kernel.js +135 -25
  29. package/src/kernel/KernelContext.js +166 -0
  30. package/src/security/EntityRoleGranterAdapter.js +350 -0
  31. package/src/security/PermissionMatcher.js +148 -0
  32. package/src/security/PermissionRegistry.js +263 -0
  33. package/src/security/PersistableRoleGranterAdapter.js +618 -0
  34. package/src/security/RoleGranterAdapter.js +123 -0
  35. package/src/security/RoleGranterStorage.js +161 -0
  36. package/src/security/RolesManager.js +81 -0
  37. package/src/security/SecurityModule.js +73 -0
  38. package/src/security/StaticRoleGranterAdapter.js +114 -0
  39. package/src/security/UsersManager.js +122 -0
  40. package/src/security/index.js +45 -0
  41. package/src/security/pages/RoleForm.vue +212 -0
  42. package/src/security/pages/RoleList.vue +106 -0
  43. package/src/styles/main.css +62 -2
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Security module - Permission and role management
3
+ *
4
+ * Provides:
5
+ * - PermissionMatcher: Wildcard pattern matching (*, **)
6
+ * - PermissionRegistry: Central registry for module permissions
7
+ * - RoleGranterAdapter: Interface for role → permissions mapping
8
+ * - StaticRoleGranterAdapter: Config-based role granter (default)
9
+ * - EntityRoleGranterAdapter: Entity-based role granter (for UI management)
10
+ * - PersistableRoleGranterAdapter: Load/persist from any source (localStorage, API)
11
+ * - SecurityModule: System module for roles UI (uses RolesManager)
12
+ * - RolesManager: System entity manager for roles
13
+ * - UsersManager: System entity manager for users (linked to roles)
14
+ *
15
+ * @example
16
+ * import {
17
+ * PermissionMatcher,
18
+ * PermissionRegistry,
19
+ * StaticRoleGranterAdapter,
20
+ * EntityRoleGranterAdapter,
21
+ * PersistableRoleGranterAdapter,
22
+ * createLocalStorageRoleGranter,
23
+ * UsersManager
24
+ * } from 'qdadm/security'
25
+ */
26
+
27
+ export { PermissionMatcher } from './PermissionMatcher.js'
28
+ export { PermissionRegistry } from './PermissionRegistry.js'
29
+ export { RoleGranterAdapter } from './RoleGranterAdapter.js'
30
+ export { StaticRoleGranterAdapter } from './StaticRoleGranterAdapter.js'
31
+ export { EntityRoleGranterAdapter } from './EntityRoleGranterAdapter.js'
32
+ export {
33
+ PersistableRoleGranterAdapter,
34
+ createLocalStorageRoleGranter
35
+ } from './PersistableRoleGranterAdapter.js'
36
+ export { SecurityModule } from './SecurityModule.js'
37
+ export { RolesManager } from './RolesManager.js'
38
+ export { UsersManager } from './UsersManager.js'
39
+ export { RoleGranterStorage } from './RoleGranterStorage.js'
40
+
41
+ /**
42
+ * Standard entity actions for CRUD operations
43
+ * Used by PermissionRegistry.registerEntity()
44
+ */
45
+ export const ENTITY_ACTIONS = ['read', 'list', 'create', 'update', 'delete']
@@ -0,0 +1,212 @@
1
+ <script setup>
2
+ /**
3
+ * RoleForm - Role create/edit form (standard FormPage pattern)
4
+ */
5
+
6
+ import { ref, computed, inject } from 'vue'
7
+ import { useFormPageBuilder, FormPage, useOrchestrator, PermissionEditor } from '../../index.js'
8
+ import InputText from 'primevue/inputtext'
9
+ import AutoComplete from 'primevue/autocomplete'
10
+ import Chip from 'primevue/chip'
11
+
12
+ // ============ FORM BUILDER ============
13
+ const form = useFormPageBuilder({ entity: 'roles' })
14
+
15
+ // ============ HELPERS ============
16
+ const { getManager } = useOrchestrator()
17
+ const manager = getManager('roles')
18
+
19
+ // Get permissionRegistry directly from Kernel (via provide/inject)
20
+ const permissionRegistry = inject('qdadmPermissionRegistry', null)
21
+
22
+ // Role options for inheritance (exclude self)
23
+ const allRoles = computed(() => {
24
+ const currentName = form.data.value?.name
25
+ const roles = manager?.roleGranter?.getRoles() || []
26
+ return roles
27
+ .filter(name => name !== currentName)
28
+ .map(name => {
29
+ const role = manager?.roleGranter?.getRole?.(name)
30
+ return {
31
+ name,
32
+ label: role?.label || name,
33
+ display: `${name} (${role?.label || name})`
34
+ }
35
+ })
36
+ })
37
+
38
+ // Autocomplete for roles
39
+ const roleInput = ref('')
40
+ const roleSuggestions = ref([])
41
+
42
+ function searchRoles(event) {
43
+ const query = (event.query || '').toLowerCase()
44
+ const selected = form.data.value.inherits || []
45
+
46
+ roleSuggestions.value = allRoles.value
47
+ .filter(r => !selected.includes(r.name))
48
+ .filter(r =>
49
+ query === '' ||
50
+ r.name.toLowerCase().includes(query) ||
51
+ r.label.toLowerCase().includes(query)
52
+ )
53
+ }
54
+
55
+ function onRoleSelect(event) {
56
+ const role = event.value
57
+ if (role && !form.data.value.inherits?.includes(role.name)) {
58
+ form.data.value.inherits = [...(form.data.value.inherits || []), role.name]
59
+ }
60
+ roleInput.value = ''
61
+ }
62
+
63
+ function removeRole(roleName) {
64
+ form.data.value.inherits = (form.data.value.inherits || []).filter(r => r !== roleName)
65
+ }
66
+
67
+ function getRoleLabel(roleName) {
68
+ const role = allRoles.value.find(r => r.name === roleName)
69
+ return role?.label || roleName
70
+ }
71
+ </script>
72
+
73
+ <template>
74
+ <FormPage v-bind="form.props.value" v-on="form.events">
75
+ <template #fields>
76
+ <div class="role-form-fields">
77
+ <!-- Role Name -->
78
+ <div class="form-field">
79
+ <label class="font-medium">Role Name</label>
80
+ <InputText
81
+ v-model="form.data.value.name"
82
+ :disabled="form.isEdit.value"
83
+ placeholder="ROLE_ADMIN"
84
+ class="w-full"
85
+ />
86
+ <small class="field-hint">Convention: ROLE_UPPERCASE_NAME</small>
87
+ </div>
88
+
89
+ <!-- Label -->
90
+ <div class="form-field">
91
+ <label class="font-medium">Display Label</label>
92
+ <InputText
93
+ v-model="form.data.value.label"
94
+ placeholder="Administrator"
95
+ class="w-full"
96
+ />
97
+ </div>
98
+
99
+ <!-- Inherits -->
100
+ <div class="form-field">
101
+ <label class="font-medium">Inherits From</label>
102
+ <div class="inherits-editor">
103
+ <div v-if="(form.data.value.inherits || []).length > 0" class="inherits-chips">
104
+ <Chip
105
+ v-for="roleName in form.data.value.inherits"
106
+ :key="roleName"
107
+ removable
108
+ @remove="removeRole(roleName)"
109
+ class="inherits-chip"
110
+ >
111
+ <span class="chip-name">{{ roleName }}</span>
112
+ <span class="chip-label">({{ getRoleLabel(roleName) }})</span>
113
+ </Chip>
114
+ </div>
115
+ <AutoComplete
116
+ v-model="roleInput"
117
+ :suggestions="roleSuggestions"
118
+ optionLabel="display"
119
+ placeholder="Type role name..."
120
+ :minLength="0"
121
+ completeOnFocus
122
+ @complete="searchRoles"
123
+ @item-select="onRoleSelect"
124
+ dropdown
125
+ class="w-full"
126
+ >
127
+ <template #option="{ option }">
128
+ <div class="role-option">
129
+ <span class="role-name">{{ option.name }}</span>
130
+ <span class="role-label">{{ option.label }}</span>
131
+ </div>
132
+ </template>
133
+ </AutoComplete>
134
+ </div>
135
+ <small class="field-hint">
136
+ This role inherits all permissions from selected roles
137
+ </small>
138
+ </div>
139
+
140
+ <!-- Permissions -->
141
+ <div class="form-field">
142
+ <label class="font-medium">
143
+ Permissions ({{ (form.data.value.permissions || []).length }})
144
+ </label>
145
+ <PermissionEditor
146
+ v-model="form.data.value.permissions"
147
+ :permissionRegistry="permissionRegistry"
148
+ placeholder="Type namespace:action..."
149
+ />
150
+ </div>
151
+ </div>
152
+ </template>
153
+ </FormPage>
154
+ </template>
155
+
156
+ <style scoped>
157
+ /* Form layout - .form-field and .field-hint are global (main.css) */
158
+ .role-form-fields {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 1.5rem;
162
+ }
163
+
164
+ /* Inherits editor - uses .editor-box pattern */
165
+ .inherits-editor {
166
+ border: 1px solid var(--p-surface-200);
167
+ border-radius: 0.5rem;
168
+ padding: 0.75rem;
169
+ background: var(--p-surface-50);
170
+ }
171
+
172
+ /* Inherits chips - uses .editor-chips pattern */
173
+ .inherits-chips {
174
+ display: flex;
175
+ flex-wrap: wrap;
176
+ gap: 0.5rem;
177
+ margin-bottom: 0.75rem;
178
+ padding-bottom: 0.75rem;
179
+ border-bottom: 1px solid var(--p-surface-200);
180
+ }
181
+
182
+ .inherits-chip {
183
+ font-family: monospace;
184
+ font-size: 0.875rem;
185
+ }
186
+
187
+ .chip-name {
188
+ font-weight: 500;
189
+ }
190
+
191
+ .chip-label {
192
+ color: var(--p-surface-500);
193
+ margin-left: 0.25rem;
194
+ }
195
+
196
+ .role-option {
197
+ display: flex;
198
+ justify-content: space-between;
199
+ align-items: center;
200
+ gap: 1rem;
201
+ }
202
+
203
+ .role-name {
204
+ font-family: monospace;
205
+ font-weight: 500;
206
+ }
207
+
208
+ .role-label {
209
+ color: var(--p-surface-500);
210
+ font-size: 0.875rem;
211
+ }
212
+ </style>
@@ -0,0 +1,106 @@
1
+ <script setup>
2
+ /**
3
+ * RoleList - Role listing page (standard ListPage pattern)
4
+ */
5
+
6
+ import { useListPageBuilder, ListPage, useOrchestrator } from '../../index.js'
7
+ import Column from 'primevue/column'
8
+ import Tag from 'primevue/tag'
9
+ import Chip from 'primevue/chip'
10
+ import Message from 'primevue/message'
11
+
12
+ // ============ LIST BUILDER ============
13
+ const list = useListPageBuilder({ entity: 'roles' })
14
+
15
+ // ============ SEARCH ============
16
+ list.setSearch({
17
+ placeholder: 'Search roles...',
18
+ fields: ['name', 'label']
19
+ })
20
+
21
+ // ============ HEADER ACTIONS ============
22
+ list.addCreateAction('New Role')
23
+
24
+ // ============ ROW ACTIONS ============
25
+ list.addEditAction()
26
+ list.addDeleteAction({ labelField: 'label' })
27
+
28
+ // ============ HELPERS ============
29
+ const { getManager } = useOrchestrator()
30
+ const manager = getManager('roles')
31
+ const canPersist = manager?.canPersist ?? false
32
+ </script>
33
+
34
+ <template>
35
+ <ListPage v-bind="list.props.value" v-on="list.events">
36
+ <!-- Read-only Warning -->
37
+ <template #before-table>
38
+ <Message v-if="!canPersist" severity="info" :closable="false" class="mb-4">
39
+ <div class="flex align-items-center gap-2">
40
+ <i class="pi pi-info-circle text-xl"></i>
41
+ <span>
42
+ <strong>Read-only:</strong> Roles are configured statically.
43
+ Use a PersistableRoleGranterAdapter for editing.
44
+ </span>
45
+ </div>
46
+ </Message>
47
+ </template>
48
+
49
+ <template #columns>
50
+ <Column field="name" header="Role" sortable style="width: 25%">
51
+ <template #body="{ data }">
52
+ <div>
53
+ <code class="role-name">{{ data.name }}</code>
54
+ <div v-if="data.label && data.label !== data.name" class="text-sm text-color-secondary mt-1">
55
+ {{ data.label }}
56
+ </div>
57
+ </div>
58
+ </template>
59
+ </Column>
60
+
61
+ <Column header="Inherits" style="width: 20%">
62
+ <template #body="{ data }">
63
+ <div v-if="data.inherits?.length" class="flex flex-wrap gap-1">
64
+ <Tag
65
+ v-for="parent in data.inherits"
66
+ :key="parent"
67
+ :value="parent"
68
+ severity="secondary"
69
+ />
70
+ </div>
71
+ <span v-else class="text-color-secondary">-</span>
72
+ </template>
73
+ </Column>
74
+
75
+ <Column header="Permissions" style="width: 40%">
76
+ <template #body="{ data }">
77
+ <div v-if="data.permissions?.length" class="flex flex-wrap gap-1">
78
+ <Chip
79
+ v-for="perm in data.permissions.slice(0, 5)"
80
+ :key="perm"
81
+ :label="perm"
82
+ class="text-xs"
83
+ />
84
+ <Chip
85
+ v-if="data.permissions.length > 5"
86
+ :label="`+${data.permissions.length - 5} more`"
87
+ class="text-xs"
88
+ />
89
+ </div>
90
+ <span v-else class="text-color-secondary">No permissions</span>
91
+ </template>
92
+ </Column>
93
+ </template>
94
+ </ListPage>
95
+ </template>
96
+
97
+ <style scoped>
98
+ .role-name {
99
+ padding: 0.2rem 0.4rem;
100
+ background: var(--p-surface-100);
101
+ border-radius: 4px;
102
+ font-size: 0.875rem;
103
+ color: var(--p-primary-color);
104
+ font-weight: 500;
105
+ }
106
+ </style>
@@ -20,6 +20,18 @@ html, body, #app {
20
20
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
21
21
  }
22
22
 
23
+ /* Global placeholder styling - light blue-gray for better visibility */
24
+ input::placeholder,
25
+ textarea::placeholder,
26
+ .p-inputtext::placeholder,
27
+ .p-autocomplete-input::placeholder,
28
+ .p-textarea::placeholder,
29
+ .p-select-label.p-placeholder,
30
+ .p-dropdown-label.p-placeholder {
31
+ color: #a0aec0 !important;
32
+ opacity: 1;
33
+ }
34
+
23
35
  /* Layout - AppLayout styles are now scoped in AppLayout.vue */
24
36
  /* Only keep truly global layout utilities here */
25
37
 
@@ -181,13 +193,61 @@ html, body, #app {
181
193
  flex: 1;
182
194
  }
183
195
 
184
- /* Field hint */
196
+ /* Field hint - small helper text below inputs */
185
197
  .field-hint,
186
198
  .form-hint {
187
- font-size: 0.75rem;
199
+ font-size: 0.8rem;
200
+ color: var(--p-surface-400);
201
+ margin-top: 0.25rem;
202
+ display: block;
203
+ }
204
+
205
+ /* Field error - validation error text */
206
+ .field-error {
207
+ font-size: 0.8rem;
208
+ color: var(--p-red-500);
209
+ margin-top: 0.25rem;
210
+ display: block;
211
+ }
212
+
213
+ /* Editor box - container for complex editors (permissions, scopes, etc.) */
214
+ .editor-box {
215
+ border: 1px solid var(--p-surface-200);
216
+ border-radius: 0.5rem;
217
+ padding: 0.75rem;
218
+ background: var(--p-surface-50);
219
+ }
220
+
221
+ /* Chips container with separator */
222
+ .editor-chips {
223
+ display: flex;
224
+ flex-wrap: wrap;
225
+ gap: 0.5rem;
226
+ margin-bottom: 0.75rem;
227
+ padding-bottom: 0.75rem;
228
+ border-bottom: 1px solid var(--p-surface-200);
229
+ }
230
+
231
+ /* Chip typography for code-like content */
232
+ .chip-code {
233
+ font-family: monospace;
234
+ font-size: 0.875rem;
235
+ }
236
+
237
+ .chip-namespace {
188
238
  color: var(--p-surface-500);
189
239
  }
190
240
 
241
+ .chip-action {
242
+ color: var(--p-primary-600);
243
+ font-weight: 500;
244
+ }
245
+
246
+ .chip-wildcard {
247
+ color: var(--p-orange-500);
248
+ font-weight: 700;
249
+ }
250
+
191
251
  /* Info grid for readonly data */
192
252
  .info-grid {
193
253
  display: grid;