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.
- package/README.md +27 -174
- package/package.json +2 -1
- package/src/auth/SessionAuthAdapter.js +114 -3
- package/src/components/editors/PermissionEditor.vue +535 -0
- package/src/components/forms/FormField.vue +1 -11
- package/src/components/index.js +1 -0
- package/src/components/layout/AppLayout.vue +20 -8
- package/src/components/layout/defaults/DefaultToaster.vue +3 -3
- package/src/components/pages/LoginPage.vue +26 -5
- package/src/composables/useCurrentEntity.js +26 -17
- package/src/composables/useForm.js +7 -0
- package/src/composables/useFormPageBuilder.js +7 -0
- package/src/composables/useNavContext.js +30 -16
- package/src/core/index.js +0 -3
- package/src/debug/AuthCollector.js +199 -31
- package/src/debug/Collector.js +24 -2
- package/src/debug/EntitiesCollector.js +8 -0
- package/src/debug/SignalCollector.js +60 -2
- package/src/debug/components/panels/AuthPanel.vue +159 -27
- package/src/debug/components/panels/EntitiesPanel.vue +18 -2
- package/src/entity/EntityManager.js +205 -36
- package/src/entity/auth/EntityAuthAdapter.js +54 -46
- package/src/entity/auth/SecurityChecker.js +110 -42
- package/src/entity/auth/factory.js +11 -2
- package/src/entity/auth/factory.test.js +29 -0
- package/src/entity/storage/factory.test.js +6 -5
- package/src/index.js +3 -0
- package/src/kernel/Kernel.js +135 -25
- package/src/kernel/KernelContext.js +166 -0
- package/src/security/EntityRoleGranterAdapter.js +350 -0
- package/src/security/PermissionMatcher.js +148 -0
- package/src/security/PermissionRegistry.js +263 -0
- package/src/security/PersistableRoleGranterAdapter.js +618 -0
- package/src/security/RoleGranterAdapter.js +123 -0
- package/src/security/RoleGranterStorage.js +161 -0
- package/src/security/RolesManager.js +81 -0
- package/src/security/SecurityModule.js +73 -0
- package/src/security/StaticRoleGranterAdapter.js +114 -0
- package/src/security/UsersManager.js +122 -0
- package/src/security/index.js +45 -0
- package/src/security/pages/RoleForm.vue +212 -0
- package/src/security/pages/RoleList.vue +106 -0
- 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
|
-
|
|
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
|
-
##
|
|
17
|
+
## Exports
|
|
16
18
|
|
|
17
19
|
```js
|
|
18
|
-
|
|
19
|
-
import
|
|
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
|
-
|
|
41
|
-
|
|
23
|
+
// Storage backends
|
|
24
|
+
import { MockApiStorage, ApiStorage, SdkStorage } from 'qdadm'
|
|
42
25
|
|
|
43
|
-
|
|
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 {
|
|
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.)
|
|
68
|
-
|
|
69
|
-
### Basic Usage
|
|
44
|
+
Adapter for generated SDK clients (hey-api, openapi-generator, etc.):
|
|
70
45
|
|
|
71
46
|
```js
|
|
72
|
-
import {
|
|
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: '
|
|
81
|
-
get: '
|
|
82
|
-
create: '
|
|
83
|
-
update: '
|
|
84
|
-
delete: '
|
|
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
|
-
|
|
61
|
+
Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSidePagination`.
|
|
126
62
|
|
|
127
|
-
|
|
63
|
+
## Documentation
|
|
128
64
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
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
|
-
|
|
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
|