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
package/README.md CHANGED
@@ -2,61 +2,38 @@
2
2
 
3
3
  **Vue 3 admin framework. PrimeVue. Zero boilerplate.**
4
4
 
5
- Full documentation: [../../README.md](../../README.md)
5
+ Quick start: [../../README.md](../../README.md) (5 min tutorial)
6
+
7
+ Concepts & patterns: [QDADM_CREDO.md](QDADM_CREDO.md)
6
8
 
7
9
  Changelog: [../../CHANGELOG.md](../../CHANGELOG.md)
8
10
 
9
11
  ## Installation
10
12
 
11
13
  ```bash
12
- npm install qdadm
14
+ npm install qdadm primevue @primeuix/themes
13
15
  ```
14
16
 
15
- ## Quick Start
17
+ ## Exports
16
18
 
17
19
  ```js
18
- import { Kernel, EntityManager, LocalStorage } from 'qdadm'
19
- import PrimeVue from 'primevue/config'
20
- import Aura from '@primeuix/themes/aura'
21
- import 'qdadm/styles'
22
-
23
- const kernel = new Kernel({
24
- root: App,
25
- modules: import.meta.glob('./modules/*/init.js', { eager: true }),
26
- managers: {
27
- books: new EntityManager({
28
- name: 'books',
29
- storage: new LocalStorage({ key: 'books' }),
30
- labelField: 'title'
31
- })
32
- },
33
- authAdapter,
34
- pages: { login: LoginPage, layout: MainLayout },
35
- homeRoute: 'book',
36
- app: { name: 'My App' },
37
- primevue: { plugin: PrimeVue, theme: Aura }
38
- })
20
+ // Core
21
+ import { Kernel, EntityManager } from 'qdadm'
39
22
 
40
- kernel.createApp().mount('#app')
41
- ```
23
+ // Storage backends
24
+ import { MockApiStorage, ApiStorage, SdkStorage } from 'qdadm'
42
25
 
43
- ## Exports
44
-
45
- ```js
46
- // Main
47
- import { Kernel, createQdadm, EntityManager, ApiStorage, LocalStorage, SdkStorage } from 'qdadm'
26
+ // Auth
27
+ import { SessionAuthAdapter, LocalStorageSessionAuthAdapter } from 'qdadm'
48
28
 
49
29
  // Composables
50
30
  import { useForm, useBareForm, useListPageBuilder } from 'qdadm/composables'
51
31
 
52
32
  // Components
53
- import { ListPage, PageLayout, FormField, FormActions } from 'qdadm/components'
33
+ import { ListPage, PageLayout, AppLayout, FormField, FormActions } from 'qdadm/components'
54
34
 
55
35
  // Module system
56
- import { initModules, getRoutes, setSectionOrder } from 'qdadm/module'
57
-
58
- // Utilities
59
- import { formatDate, truncate } from 'qdadm/utils'
36
+ import { KernelContext } from 'qdadm/module'
60
37
 
61
38
  // Styles
62
39
  import 'qdadm/styles'
@@ -64,155 +41,32 @@ import 'qdadm/styles'
64
41
 
65
42
  ## SdkStorage
66
43
 
67
- Adapter for generated SDK clients (hey-api, openapi-generator, etc.). Maps SDK methods to standard CRUD operations with optional transforms.
68
-
69
- ### Basic Usage
44
+ Adapter for generated SDK clients (hey-api, openapi-generator, etc.):
70
45
 
71
46
  ```js
72
- import { EntityManager, SdkStorage } from 'qdadm'
73
- import { Sdk } from './generated/sdk.gen.js'
74
-
75
- const sdk = new Sdk({ client: myClient })
47
+ import { SdkStorage } from 'qdadm'
76
48
 
77
49
  const storage = new SdkStorage({
78
50
  sdk,
79
51
  methods: {
80
- list: 'getApiAdminTasks',
81
- get: 'getApiAdminTasksById',
82
- create: 'postApiAdminTasks',
83
- update: 'patchApiAdminTasksById',
84
- delete: 'deleteApiAdminTasksById'
52
+ list: 'getApiBooks',
53
+ get: 'getApiBooksById',
54
+ create: 'postApiBooks',
55
+ update: 'patchApiBooksById',
56
+ delete: 'deleteApiBooksById'
85
57
  }
86
58
  })
87
-
88
- const manager = new EntityManager({
89
- name: 'tasks',
90
- storage,
91
- labelField: 'name'
92
- })
93
- ```
94
-
95
- ### Configuration Options
96
-
97
- | Option | Type | Description |
98
- |--------|------|-------------|
99
- | `sdk` | object | SDK instance |
100
- | `getSdk` | function | Callback for lazy SDK loading |
101
- | `methods` | object | Map operations to SDK method names or callbacks |
102
- | `transformRequest` | function | Global request transform `(operation, params) => params` |
103
- | `transformResponse` | function | Global response transform `(operation, data) => data` |
104
- | `transforms` | object | Per-method transforms (override global) |
105
- | `responseFormat` | object | Configure response normalization |
106
- | `clientSidePagination` | boolean | Handle pagination locally (default: false) |
107
-
108
- ### Method Mapping
109
-
110
- Methods can be strings (SDK method name) or callbacks for full control:
111
-
112
- ```js
113
- methods: {
114
- // String: calls sdk.getItems({ query: params })
115
- list: 'getItems',
116
-
117
- // Callback: full control over SDK invocation
118
- get: async (sdk, id) => {
119
- const result = await sdk.getItemById({ path: { id } })
120
- return result.data
121
- }
122
- }
123
59
  ```
124
60
 
125
- ### Transform Callbacks
61
+ Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSidePagination`.
126
62
 
127
- **Global transforms** apply to all operations:
63
+ ## Documentation
128
64
 
129
- ```js
130
- new SdkStorage({
131
- sdk,
132
- methods: { list: 'getItems', get: 'getItemById' },
133
- transformRequest: (operation, params) => {
134
- if (operation === 'list') {
135
- return { query: params }
136
- }
137
- return params
138
- },
139
- transformResponse: (operation, response) => response.data
140
- })
141
- ```
142
-
143
- **Per-method transforms** override global ones:
144
-
145
- ```js
146
- new SdkStorage({
147
- sdk,
148
- methods: { list: 'getItems' },
149
- transforms: {
150
- list: {
151
- request: (params) => ({ query: { ...params, active: true } }),
152
- response: (data) => ({ items: data.results, total: data.count })
153
- }
154
- }
155
- })
156
- ```
157
-
158
- ### Response Format Normalization
159
-
160
- For APIs with non-standard response shapes, configure normalization before transforms:
161
-
162
- ```js
163
- new SdkStorage({
164
- sdk,
165
- methods: { list: 'getItems' },
166
- responseFormat: {
167
- dataField: 'results', // Field containing array (e.g., 'data', 'results')
168
- totalField: 'count', // Field for total count (null = compute from array)
169
- itemsField: 'data.items' // Nested path (takes precedence over dataField)
170
- }
171
- })
172
- ```
173
-
174
- ### Client-Side Pagination
175
-
176
- For SDKs that return all items without server-side pagination:
177
-
178
- ```js
179
- new SdkStorage({
180
- sdk,
181
- methods: { list: 'getAllItems' },
182
- clientSidePagination: true // Fetches all, paginates/sorts/filters in-memory
183
- })
184
- ```
185
-
186
- ### Real-World Example (hey-api SDK)
187
-
188
- ```js
189
- import { Sdk } from '@/generated/sdk.gen.js'
190
- import { client } from '@/generated/client.gen.js'
191
-
192
- // Configure client
193
- client.setConfig({ baseUrl: '/api' })
194
-
195
- const sdk = new Sdk({ client })
196
-
197
- const taskStorage = new SdkStorage({
198
- sdk,
199
- methods: {
200
- list: 'getApiAdminTasks',
201
- get: 'getApiAdminTasksById',
202
- create: 'postApiAdminTasks',
203
- patch: 'patchApiAdminTasksById',
204
- delete: 'deleteApiAdminTasksById'
205
- },
206
- transforms: {
207
- list: {
208
- response: (data) => ({
209
- items: data.items.map(t => ({ ...t, statusLabel: t.status.toUpperCase() })),
210
- total: data.total
211
- })
212
- }
213
- }
214
- })
215
- ```
65
+ | Doc | Purpose |
66
+ |-----|---------|
67
+ | [QDADM_CREDO](QDADM_CREDO.md) | Philosophy, patterns, concepts |
68
+ | [Architecture](docs/architecture.md) | PAC pattern, layers |
69
+ | [Extension](docs/extension.md) | Hooks, signals, zones |
216
70
 
217
71
  ## Peer Dependencies
218
72
 
@@ -220,7 +74,6 @@ const taskStorage = new SdkStorage({
220
74
  - vue-router ^4.0.0
221
75
  - primevue ^4.0.0
222
76
  - pinia ^2.0.0
223
- - vanilla-jsoneditor ^0.23.0
224
77
 
225
78
  ## License
226
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "0.35.0",
3
+ "version": "0.38.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
  "exports": {
22
22
  ".": "./src/index.js",
23
23
  "./auth": "./src/auth/index.js",
24
+ "./security": "./src/security/index.js",
24
25
  "./composables": "./src/composables/index.js",
25
26
  "./components": "./src/components/index.js",
26
27
  "./editors": "./src/editors/index.js",
@@ -196,6 +196,7 @@ export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
196
196
  constructor(storageKey = 'qdadm_auth') {
197
197
  super()
198
198
  this._storageKey = storageKey
199
+ this._originalUser = null // Stores original user during impersonation
199
200
  this._restore()
200
201
  }
201
202
 
@@ -207,9 +208,10 @@ export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
207
208
  try {
208
209
  const stored = localStorage.getItem(this._storageKey)
209
210
  if (stored) {
210
- const { token, user } = JSON.parse(stored)
211
+ const { token, user, originalUser } = JSON.parse(stored)
211
212
  this._token = token
212
213
  this._user = user
214
+ this._originalUser = originalUser || null
213
215
  }
214
216
  } catch {
215
217
  // Invalid stored data, ignore
@@ -223,10 +225,14 @@ export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
223
225
  */
224
226
  persist() {
225
227
  if (this._token && this._user) {
226
- localStorage.setItem(this._storageKey, JSON.stringify({
228
+ const data = {
227
229
  token: this._token,
228
230
  user: this._user
229
- }))
231
+ }
232
+ if (this._originalUser) {
233
+ data.originalUser = this._originalUser
234
+ }
235
+ localStorage.setItem(this._storageKey, JSON.stringify(data))
230
236
  } else {
231
237
  localStorage.removeItem(this._storageKey)
232
238
  }
@@ -234,6 +240,7 @@ export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
234
240
 
235
241
  // Arrow functions to preserve `this` when used as callbacks
236
242
  logout = () => {
243
+ this._originalUser = null
237
244
  this.clearSession()
238
245
  localStorage.removeItem(this._storageKey)
239
246
  }
@@ -249,6 +256,110 @@ export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
249
256
  getUser = () => {
250
257
  return this._user
251
258
  }
259
+
260
+ // ─────────────────────────────────────────────────────────────────
261
+ // Impersonation support
262
+ // ─────────────────────────────────────────────────────────────────
263
+
264
+ /**
265
+ * Check if currently impersonating another user
266
+ * @returns {boolean}
267
+ */
268
+ isImpersonating = () => {
269
+ return this._originalUser !== null
270
+ }
271
+
272
+ /**
273
+ * Get the original user (admin) when impersonating
274
+ * @returns {object|null} Original user or null if not impersonating
275
+ */
276
+ getOriginalUser = () => {
277
+ return this._originalUser
278
+ }
279
+
280
+ /**
281
+ * Start impersonating another user
282
+ *
283
+ * @param {object} targetUser - User to impersonate
284
+ * @returns {object} The impersonated user
285
+ */
286
+ impersonate = (targetUser) => {
287
+ if (!this._user) {
288
+ throw new Error('Must be authenticated to impersonate')
289
+ }
290
+ if (this._originalUser) {
291
+ throw new Error('Already impersonating. Stop first.')
292
+ }
293
+
294
+ this._originalUser = this._user
295
+ this._user = targetUser
296
+ this.persist()
297
+
298
+ return targetUser
299
+ }
300
+
301
+ /**
302
+ * Stop impersonating and return to original user
303
+ *
304
+ * @returns {object} The original user
305
+ */
306
+ stopImpersonating = () => {
307
+ if (!this._originalUser) {
308
+ throw new Error('Not currently impersonating')
309
+ }
310
+
311
+ const original = this._originalUser
312
+ this._user = this._originalUser
313
+ this._originalUser = null
314
+ this.persist()
315
+
316
+ return original
317
+ }
318
+
319
+ // ─────────────────────────────────────────────────────────────────
320
+ // Signal integration
321
+ // ─────────────────────────────────────────────────────────────────
322
+
323
+ /**
324
+ * Connect authAdapter to signals for reactive impersonation
325
+ *
326
+ * Call this during kernel boot to enable signal-driven impersonation.
327
+ * The adapter will listen to auth:impersonate and auth:impersonate:stop
328
+ * signals and update its internal state automatically.
329
+ *
330
+ * @param {object} signals - Signals instance (from orchestrator.signals)
331
+ * @returns {Function} Cleanup function to unsubscribe
332
+ */
333
+ connectSignals(signals) {
334
+ if (!signals) return () => {}
335
+
336
+ const cleanups = []
337
+
338
+ // Listen for impersonate signal
339
+ cleanups.push(signals.on('auth:impersonate', (event) => {
340
+ const data = event?.data || event
341
+ const targetUser = data?.target
342
+ if (targetUser && !this._originalUser) {
343
+ this._originalUser = this._user
344
+ this._user = targetUser
345
+ this.persist()
346
+ }
347
+ }))
348
+
349
+ // Listen for stop impersonation signal
350
+ cleanups.push(signals.on('auth:impersonate:stop', () => {
351
+ if (this._originalUser) {
352
+ this._user = this._originalUser
353
+ this._originalUser = null
354
+ this.persist()
355
+ }
356
+ }))
357
+
358
+ // Return cleanup function
359
+ return () => {
360
+ cleanups.forEach(cleanup => cleanup?.())
361
+ }
362
+ }
252
363
  }
253
364
 
254
365
  export default SessionAuthAdapter