@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * useUserImpersonator - User impersonation composable
3
+ *
4
+ * Provides state and actions for user impersonation.
5
+ * Useful for admin dashboards to test permission-based UI as different users.
6
+ *
7
+ * Permission check:
8
+ * - Requires 'auth:impersonate' permission (check via entityAuthAdapter.isGranted())
9
+ * - Falls back to true if no entityAuthAdapter
10
+ *
11
+ * Signals emitted:
12
+ * - auth:impersonate({ target, original }) - when impersonation starts
13
+ * - auth:impersonate:stop({ original }) - when impersonation ends
14
+ *
15
+ * Usage:
16
+ * // With users entity
17
+ * const { isImpersonating, users, impersonate, exit } = useUserImpersonator({ entity: 'users' })
18
+ *
19
+ * // With direct users list
20
+ * const { isImpersonating, impersonate, exit } = useUserImpersonator({ users: myUsersList })
21
+ *
22
+ * // Custom user mapping
23
+ * const { ... } = useUserImpersonator({
24
+ * entity: 'users',
25
+ * userKey: 'uuid', // default: 'id'
26
+ * userLabel: 'email', // default: 'username'
27
+ * })
28
+ */
29
+
30
+ import {
31
+ ref,
32
+ computed,
33
+ onMounted,
34
+ inject,
35
+ getCurrentInstance,
36
+ unref,
37
+ type Ref,
38
+ type ComputedRef,
39
+ } from 'vue'
40
+ import type { SignalBus } from '../kernel/SignalBus'
41
+
42
+ /**
43
+ * User record type
44
+ */
45
+ export interface UserRecord {
46
+ [key: string]: unknown
47
+ role?: string
48
+ }
49
+
50
+ /**
51
+ * Auth adapter interface for impersonation
52
+ */
53
+ interface AuthAdapterWithImpersonation {
54
+ getUser?: () => UserRecord | null
55
+ getOriginalUser?: () => UserRecord | null
56
+ }
57
+
58
+ /**
59
+ * Entity auth adapter interface
60
+ */
61
+ interface EntityAuthAdapter {
62
+ isGranted?: (permission: string) => boolean
63
+ }
64
+
65
+ /**
66
+ * Orchestrator interface
67
+ */
68
+ interface Orchestrator {
69
+ signals?: SignalBus
70
+ entityAuthAdapter?: EntityAuthAdapter
71
+ get: (entityName: string) => EntityManager | null
72
+ }
73
+
74
+ /**
75
+ * Entity manager interface
76
+ */
77
+ interface EntityManager {
78
+ findAll: () => Promise<{ data?: UserRecord[] } | UserRecord[]>
79
+ }
80
+
81
+ /**
82
+ * Options for useUserImpersonator
83
+ */
84
+ export interface UseUserImpersonatorOptions {
85
+ /** Entity name to fetch users from (e.g., 'users') */
86
+ entity?: string
87
+ /** Direct users list (reactive or static) */
88
+ users?: UserRecord[] | Ref<UserRecord[]>
89
+ /** Field to use as user ID (default: 'id') */
90
+ userKey?: string
91
+ /** Field to use as user display label (default: 'username') */
92
+ userLabel?: string
93
+ /** Permission to check for impersonation (default: 'auth:impersonate') */
94
+ permission?: string
95
+ }
96
+
97
+ /**
98
+ * Return type for useUserImpersonator
99
+ */
100
+ export interface UseUserImpersonatorReturn {
101
+ // State
102
+ users: Ref<UserRecord[]>
103
+ loading: Ref<boolean>
104
+ error: Ref<string | null>
105
+ currentUser: ComputedRef<UserRecord | null>
106
+ originalUser: ComputedRef<UserRecord | null>
107
+ isImpersonating: ComputedRef<boolean>
108
+ canImpersonate: ComputedRef<boolean>
109
+ availableUsers: ComputedRef<UserRecord[]>
110
+
111
+ // Actions
112
+ impersonate: (userOrId: UserRecord | string | number) => Promise<void>
113
+ exitImpersonation: () => Promise<void>
114
+ exit: () => Promise<void>
115
+ loadUsers: () => Promise<void>
116
+
117
+ // Config
118
+ userKey: string
119
+ userLabel: string
120
+ }
121
+
122
+ /**
123
+ * User impersonation composable
124
+ *
125
+ * @param options - Configuration options
126
+ * @returns Impersonation state and actions
127
+ */
128
+ export function useUserImpersonator(
129
+ options: UseUserImpersonatorOptions = {}
130
+ ): UseUserImpersonatorReturn {
131
+ // Strict context check - must be called in component setup()
132
+ const instance = getCurrentInstance()
133
+ if (!instance) {
134
+ throw new Error('[qdadm] useUserImpersonator() must be called within component setup()')
135
+ }
136
+
137
+ const {
138
+ entity,
139
+ users: providedUsers,
140
+ userKey = 'id',
141
+ userLabel = 'username',
142
+ permission = 'auth:impersonate',
143
+ } = options
144
+
145
+ const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
146
+ const authAdapter = inject<AuthAdapterWithImpersonation | null>('authAdapter', null)
147
+ const signals = orchestrator?.signals || null
148
+ const entityAuth = orchestrator?.entityAuthAdapter || null
149
+
150
+ // State
151
+ const users = ref<UserRecord[]>([])
152
+ const loading = ref(false)
153
+ const error = ref<string | null>(null)
154
+
155
+ // Note: authTick removed - Kernel.invalidateApp() remounts entire app
156
+ // on auth changes, so composable is re-initialized with fresh state
157
+
158
+ /**
159
+ * Current user (may be impersonated)
160
+ */
161
+ const currentUser = computed(() => {
162
+ return authAdapter?.getUser?.() || null
163
+ })
164
+
165
+ /**
166
+ * Original user (before impersonation)
167
+ */
168
+ const originalUser = computed(() => {
169
+ return authAdapter?.getOriginalUser?.() || null
170
+ })
171
+
172
+ /**
173
+ * Currently impersonating another user
174
+ */
175
+ const isImpersonating = computed(() => {
176
+ return originalUser.value !== null
177
+ })
178
+
179
+ /**
180
+ * Check if current user can impersonate others
181
+ */
182
+ const canImpersonate = computed(() => {
183
+ if (!entityAuth?.isGranted) return true // Default allow if no auth
184
+ return entityAuth.isGranted(permission)
185
+ })
186
+
187
+ /**
188
+ * Users available for impersonation
189
+ * Excludes: original admin AND currently impersonated user
190
+ * Returns full user objects (not just id/label) so role is preserved
191
+ */
192
+ const availableUsers = computed(() => {
193
+ const excludeIds = new Set<unknown>()
194
+ if (originalUser.value?.[userKey]) excludeIds.add(originalUser.value[userKey])
195
+ if (currentUser.value?.[userKey]) excludeIds.add(currentUser.value[userKey])
196
+
197
+ return users.value.filter((u) => !excludeIds.has(u[userKey]))
198
+ })
199
+
200
+ /**
201
+ * Load users from entity or provided list
202
+ */
203
+ async function loadUsers(): Promise<void> {
204
+ // If users provided directly, use them
205
+ if (providedUsers) {
206
+ users.value = unref(providedUsers)
207
+ return
208
+ }
209
+
210
+ // Load from entity
211
+ if (entity && orchestrator) {
212
+ try {
213
+ loading.value = true
214
+ error.value = null
215
+ const manager = orchestrator.get(entity)
216
+ if (manager) {
217
+ const result = await manager.findAll()
218
+ users.value =
219
+ (result as { data?: UserRecord[] })?.data ||
220
+ (result as UserRecord[]) ||
221
+ ([] as UserRecord[])
222
+ }
223
+ } catch (e) {
224
+ error.value = (e as Error).message
225
+ console.warn(`[qdadm] useUserImpersonator: Failed to load users from ${entity}:`, e)
226
+ } finally {
227
+ loading.value = false
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Start impersonating a user
234
+ *
235
+ * @param userOrId - User object or user ID to impersonate
236
+ */
237
+ async function impersonate(userOrId: UserRecord | string | number): Promise<void> {
238
+ if (!canImpersonate.value) {
239
+ console.warn('[qdadm] useUserImpersonator: No permission to impersonate')
240
+ return
241
+ }
242
+
243
+ // Resolve user object
244
+ let targetUser: UserRecord | undefined
245
+ if (typeof userOrId === 'object') {
246
+ targetUser = userOrId
247
+ } else {
248
+ // Find user by ID
249
+ targetUser = users.value.find((u) => u[userKey] === userOrId)
250
+ }
251
+
252
+ if (!targetUser) {
253
+ console.warn('[qdadm] useUserImpersonator: User not found')
254
+ return
255
+ }
256
+
257
+ // Get current user for signal payload
258
+ const original = authAdapter?.getUser?.()
259
+
260
+ // Build target user object with essential fields
261
+ const impersonatedUser: UserRecord = {
262
+ [userKey]: targetUser[userKey],
263
+ [userLabel]: targetUser[userLabel],
264
+ role: targetUser.role, // Include role if present
265
+ }
266
+
267
+ // Emit auth:impersonate signal - authAdapter reacts via connectSignals()
268
+ await signals?.emit('auth:impersonate', {
269
+ target: impersonatedUser,
270
+ original,
271
+ })
272
+ }
273
+
274
+ /**
275
+ * Exit impersonation and restore original user
276
+ */
277
+ async function exitImpersonation(): Promise<void> {
278
+ if (!isImpersonating.value) return
279
+
280
+ // Get original user for signal payload
281
+ const original = authAdapter?.getOriginalUser?.()
282
+
283
+ // Emit auth:impersonate:stop signal - authAdapter reacts via connectSignals()
284
+ await signals?.emit('auth:impersonate:stop', {
285
+ original,
286
+ })
287
+ }
288
+
289
+ // Load users on mount
290
+ // Note: Auth signal listeners removed - Kernel.invalidateApp() remounts entire app
291
+ // on auth changes, so composable is re-initialized with fresh state
292
+ onMounted(() => {
293
+ loadUsers()
294
+ })
295
+
296
+ return {
297
+ // State
298
+ users,
299
+ loading,
300
+ error,
301
+ currentUser,
302
+ originalUser,
303
+ isImpersonating,
304
+ canImpersonate,
305
+ availableUsers,
306
+
307
+ // Actions
308
+ impersonate,
309
+ exitImpersonation,
310
+ exit: exitImpersonation, // Alias
311
+ loadUsers,
312
+
313
+ // Config (for template binding)
314
+ userKey,
315
+ userLabel,
316
+ }
317
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * useZoneRegistry - Access the zone registry for extensible UI composition
3
+ *
4
+ * Provides reactive access to the ZoneRegistry created by Kernel during bootstrap.
5
+ * Components can register/unregister blocks and query zones reactively.
6
+ *
7
+ * **Reactivity**: The registry is reactive. When blocks are added, removed, or modified,
8
+ * components using `getBlocks()` will automatically re-render.
9
+ *
10
+ * @returns Object with registry methods and reactive helpers
11
+ * @throws Error if zone registry is not available (Kernel not initialized)
12
+ *
13
+ * @example
14
+ * // Basic access to zones (reactive)
15
+ * import { useZoneRegistry } from '@quazardous/qdadm'
16
+ * import { ZONES } from '@quazardous/qdadm/zones'
17
+ *
18
+ * const { getBlocks, registerBlock, unregisterBlock } = useZoneRegistry()
19
+ *
20
+ * // Get blocks (reactive - component re-renders when blocks change)
21
+ * const headerBlocks = computed(() => getBlocks(ZONES.HEADER))
22
+ *
23
+ * @example
24
+ * // Register/unregister blocks at component lifecycle
25
+ * import { useZoneRegistry } from '@quazardous/qdadm'
26
+ * import { ZONES } from '@quazardous/qdadm/zones'
27
+ * import AdBanner from './AdBanner.vue'
28
+ *
29
+ * const { registerBlock, unregisterBlock } = useZoneRegistry()
30
+ *
31
+ * onMounted(() => {
32
+ * registerBlock(ZONES.SIDEBAR, {
33
+ * id: 'ad-banner',
34
+ * component: AdBanner,
35
+ * weight: 50,
36
+ * props: { variant: 'compact' }
37
+ * })
38
+ * })
39
+ *
40
+ * onUnmounted(() => {
41
+ * unregisterBlock(ZONES.SIDEBAR, 'ad-banner')
42
+ * })
43
+ *
44
+ * @example
45
+ * // Conditional block registration based on feature flag
46
+ * import { useZoneRegistry } from '@quazardous/qdadm'
47
+ * import { watch } from 'vue'
48
+ *
49
+ * const { registerBlock, unregisterBlock } = useZoneRegistry()
50
+ *
51
+ * watch(featureEnabled, (enabled) => {
52
+ * if (enabled) {
53
+ * registerBlock('sidebar', { id: 'feature-widget', component: FeatureWidget })
54
+ * } else {
55
+ * unregisterBlock('sidebar', 'feature-widget')
56
+ * }
57
+ * }, { immediate: true })
58
+ *
59
+ * @example
60
+ * // Direct registry access
61
+ * const { registry } = useZoneRegistry()
62
+ * const zones = registry.listZones()
63
+ * console.log('Available zones:', zones)
64
+ */
65
+ import { inject, computed, type ComputedRef, type Component, type Ref } from 'vue'
66
+
67
+ /**
68
+ * Block configuration for zone registration
69
+ */
70
+ export interface BlockConfig {
71
+ /** Unique block identifier */
72
+ id: string
73
+ /** Vue component to render */
74
+ component: Component
75
+ /** Sort weight (lower = first) */
76
+ weight?: number
77
+ /** Props to pass to component */
78
+ props?: Record<string, unknown>
79
+ /** Additional config */
80
+ [key: string]: unknown
81
+ }
82
+
83
+ /**
84
+ * Zone definition
85
+ */
86
+ export interface ZoneDefinition {
87
+ /** Zone name */
88
+ name: string
89
+ /** Default component if no blocks registered */
90
+ default?: Component
91
+ /** Zone description */
92
+ description?: string
93
+ }
94
+
95
+ /**
96
+ * Zone info
97
+ */
98
+ export interface ZoneInfo {
99
+ name: string
100
+ blockCount: number
101
+ hasDefault: boolean
102
+ }
103
+
104
+ /**
105
+ * Zone registry interface
106
+ */
107
+ export interface ZoneRegistry {
108
+ getVersionRef: () => Ref<number>
109
+ getBlocks: (zoneName: string) => BlockConfig[]
110
+ registerBlock: (zoneName: string, blockConfig: BlockConfig) => ZoneRegistry
111
+ unregisterBlock: (zoneName: string, blockId: string) => boolean
112
+ defineZone: (zoneName: string, definition?: ZoneDefinition) => ZoneRegistry
113
+ hasBlocks: (zoneName: string) => boolean
114
+ hasZone: (zoneName: string) => boolean
115
+ getDefault: (zoneName: string) => Component | null
116
+ listZones: () => string[]
117
+ getZoneInfo: (zoneName: string) => ZoneInfo | null
118
+ inspect: () => Record<string, BlockConfig[]>
119
+ }
120
+
121
+ /**
122
+ * Return type for useZoneRegistry
123
+ */
124
+ export interface UseZoneRegistryReturn {
125
+ /** Direct registry access for advanced usage */
126
+ registry: ZoneRegistry
127
+
128
+ /** Get blocks for a zone (reactive) */
129
+ getBlocks: (zoneName: string) => BlockConfig[]
130
+ /** Create a computed ref for a zone's blocks */
131
+ useBlocks: (zoneName: string) => ComputedRef<BlockConfig[]>
132
+
133
+ /** Register a block in a zone */
134
+ registerBlock: (zoneName: string, blockConfig: BlockConfig) => ZoneRegistry
135
+ /** Unregister a block from a zone */
136
+ unregisterBlock: (zoneName: string, blockId: string) => boolean
137
+
138
+ /** Define a new zone */
139
+ defineZone: (zoneName: string, definition?: ZoneDefinition) => ZoneRegistry
140
+ /** Check if zone has any blocks */
141
+ hasBlocks: (zoneName: string) => boolean
142
+ /** Check if zone exists */
143
+ hasZone: (zoneName: string) => boolean
144
+ /** Get default component for zone */
145
+ getDefault: (zoneName: string) => Component | null
146
+ /** List all zone names */
147
+ listZones: () => string[]
148
+ /** Get zone info */
149
+ getZoneInfo: (zoneName: string) => ZoneInfo | null
150
+ /** Inspect all zones and blocks */
151
+ inspect: () => Record<string, BlockConfig[]>
152
+ }
153
+
154
+ export function useZoneRegistry(): UseZoneRegistryReturn {
155
+ const injectedRegistry = inject<ZoneRegistry | null>('qdadmZoneRegistry', null)
156
+
157
+ if (!injectedRegistry) {
158
+ throw new Error('[qdadm] Zone registry not provided. Ensure Kernel is initialized.')
159
+ }
160
+
161
+ // Non-null after check above
162
+ const registry = injectedRegistry
163
+
164
+ // Get the reactive version ref for tracking changes
165
+ const version = registry.getVersionRef()
166
+
167
+ /**
168
+ * Get blocks for a zone (reactive)
169
+ *
170
+ * This function depends on the registry's version counter,
171
+ * so it will return fresh data when blocks change.
172
+ *
173
+ * @param zoneName - Zone name
174
+ * @returns Array of block configs
175
+ */
176
+ function getBlocks(zoneName: string): BlockConfig[] {
177
+ // Access version.value to create reactive dependency
178
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
179
+ version.value
180
+ return registry.getBlocks(zoneName)
181
+ }
182
+
183
+ /**
184
+ * Create a computed ref for a zone's blocks
185
+ *
186
+ * Convenience method for creating a reactive computed ref
187
+ * that updates when the zone's blocks change.
188
+ *
189
+ * @param zoneName - Zone name
190
+ * @returns Computed ref of blocks
191
+ *
192
+ * @example
193
+ * const headerBlocks = useBlocks(ZONES.HEADER)
194
+ * // headerBlocks.value is reactive array
195
+ */
196
+ function useBlocks(zoneName: string): ComputedRef<BlockConfig[]> {
197
+ return computed(() => {
198
+ // Access version to create dependency
199
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
200
+ version.value
201
+ return registry.getBlocks(zoneName)
202
+ })
203
+ }
204
+
205
+ /**
206
+ * Register a block in a zone
207
+ *
208
+ * Proxy to registry.registerBlock() for convenience.
209
+ *
210
+ * @param zoneName - Target zone name
211
+ * @param blockConfig - Block configuration
212
+ * @returns The registry (for chaining)
213
+ */
214
+ function registerBlock(zoneName: string, blockConfig: BlockConfig): ZoneRegistry {
215
+ return registry.registerBlock(zoneName, blockConfig)
216
+ }
217
+
218
+ /**
219
+ * Unregister a block from a zone
220
+ *
221
+ * Proxy to registry.unregisterBlock() for convenience.
222
+ *
223
+ * @param zoneName - Zone name
224
+ * @param blockId - Block ID to unregister
225
+ * @returns True if block was found and removed
226
+ */
227
+ function unregisterBlock(zoneName: string, blockId: string): boolean {
228
+ return registry.unregisterBlock(zoneName, blockId)
229
+ }
230
+
231
+ return {
232
+ // Direct registry access for advanced usage
233
+ registry,
234
+
235
+ // Reactive helpers
236
+ getBlocks,
237
+ useBlocks,
238
+
239
+ // Block management
240
+ registerBlock,
241
+ unregisterBlock,
242
+
243
+ // Passthrough common methods
244
+ defineZone: registry.defineZone.bind(registry),
245
+ hasBlocks: registry.hasBlocks.bind(registry),
246
+ hasZone: registry.hasZone.bind(registry),
247
+ getDefault: registry.getDefault.bind(registry),
248
+ listZones: registry.listZones.bind(registry),
249
+ getZoneInfo: registry.getZoneInfo.bind(registry),
250
+ inspect: registry.inspect.bind(registry),
251
+ }
252
+ }