@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- 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
|
+
}
|