@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,385 @@
|
|
|
1
|
+
import { RoleProvider } from './RolesProvider'
|
|
2
|
+
import type {
|
|
3
|
+
RoleMeta,
|
|
4
|
+
RoleData,
|
|
5
|
+
RoleHierarchyMap,
|
|
6
|
+
RoleProviderContext,
|
|
7
|
+
} from './RolesProvider'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for EntityRoleProvider
|
|
11
|
+
*/
|
|
12
|
+
export interface EntityRolesProviderOptions {
|
|
13
|
+
entityName?: string
|
|
14
|
+
nameField?: string
|
|
15
|
+
labelField?: string
|
|
16
|
+
permissionsField?: string
|
|
17
|
+
inheritsField?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Role entity structure
|
|
22
|
+
*/
|
|
23
|
+
interface RoleEntity {
|
|
24
|
+
id?: string | number
|
|
25
|
+
[key: string]: unknown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Entity manager interface (minimal for adapter needs)
|
|
30
|
+
*/
|
|
31
|
+
interface EntityManager {
|
|
32
|
+
list(params?: Record<string, unknown>): Promise<{ items: RoleEntity[]; data?: RoleEntity[] }>
|
|
33
|
+
create(data: Record<string, unknown>): Promise<RoleEntity>
|
|
34
|
+
update(id: string | number, data: Record<string, unknown>): Promise<RoleEntity>
|
|
35
|
+
delete(id: string | number): Promise<void>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Orchestrator interface (minimal for adapter needs)
|
|
40
|
+
*/
|
|
41
|
+
interface Orchestrator {
|
|
42
|
+
get(name: string): EntityManager | undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cache structure for roles
|
|
47
|
+
*/
|
|
48
|
+
interface RoleCache {
|
|
49
|
+
permissions: Record<string, string[]>
|
|
50
|
+
hierarchy: Record<string, string[]>
|
|
51
|
+
labels: Record<string, string>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* EntityRoleProvider - Roles provider backed by an entity
|
|
56
|
+
*
|
|
57
|
+
* Fetches roles and permissions from a 'roles' entity.
|
|
58
|
+
* Auto-invalidates cache on entity:roles:* signals.
|
|
59
|
+
* Use this for apps with a role management UI.
|
|
60
|
+
*
|
|
61
|
+
* Expected entity schema:
|
|
62
|
+
* {
|
|
63
|
+
* id: 'role-1',
|
|
64
|
+
* name: 'ROLE_ADMIN',
|
|
65
|
+
* label: 'Administrator',
|
|
66
|
+
* permissions: ['entity:**', 'admin:**'],
|
|
67
|
+
* inherits: ['ROLE_USER']
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // Configure Kernel with entity-based roles
|
|
72
|
+
* const kernel = new Kernel({
|
|
73
|
+
* modules: [SecurityModule, ...],
|
|
74
|
+
* security: {
|
|
75
|
+
* rolesProvider: new EntityRoleProvider({
|
|
76
|
+
* entityName: 'roles',
|
|
77
|
+
* nameField: 'name',
|
|
78
|
+
* permissionsField: 'permissions',
|
|
79
|
+
* inheritsField: 'inherits'
|
|
80
|
+
* })
|
|
81
|
+
* }
|
|
82
|
+
* })
|
|
83
|
+
*
|
|
84
|
+
* // Load roles before boot
|
|
85
|
+
* await kernel.options.security.rolesProvider.load()
|
|
86
|
+
* await kernel.boot()
|
|
87
|
+
*/
|
|
88
|
+
export class EntityRoleProvider extends RoleProvider {
|
|
89
|
+
private _entityName: string
|
|
90
|
+
private _nameField: string
|
|
91
|
+
private _labelField: string
|
|
92
|
+
private _permissionsField: string
|
|
93
|
+
private _inheritsField: string
|
|
94
|
+
|
|
95
|
+
private _cache: RoleCache | null = null
|
|
96
|
+
private _ctx: RoleProviderContext | null = null
|
|
97
|
+
private _orchestrator: Orchestrator | null = null
|
|
98
|
+
private _signalCleanup: (() => void) | null = null
|
|
99
|
+
private _loading: Promise<void> | null = null
|
|
100
|
+
|
|
101
|
+
constructor(options: EntityRolesProviderOptions = {}) {
|
|
102
|
+
super()
|
|
103
|
+
this._entityName = options.entityName || 'roles'
|
|
104
|
+
this._nameField = options.nameField || 'name'
|
|
105
|
+
this._labelField = options.labelField || 'label'
|
|
106
|
+
this._permissionsField = options.permissionsField || 'permissions'
|
|
107
|
+
this._inheritsField = options.inheritsField || 'inherits'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Install adapter (called by Kernel after orchestrator ready)
|
|
112
|
+
*/
|
|
113
|
+
install(ctx: RoleProviderContext): void {
|
|
114
|
+
this._ctx = ctx
|
|
115
|
+
this._orchestrator = ctx.orchestrator as Orchestrator
|
|
116
|
+
|
|
117
|
+
// Auto-invalidate on role changes
|
|
118
|
+
if (ctx.signals) {
|
|
119
|
+
this._signalCleanup = ctx.signals.on(`entity:${this._entityName}:**`, () => {
|
|
120
|
+
this.invalidate()
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Uninstall adapter (cleanup)
|
|
127
|
+
*/
|
|
128
|
+
uninstall(): void {
|
|
129
|
+
if (this._signalCleanup) {
|
|
130
|
+
this._signalCleanup()
|
|
131
|
+
this._signalCleanup = null
|
|
132
|
+
}
|
|
133
|
+
this._ctx = null
|
|
134
|
+
this._orchestrator = null
|
|
135
|
+
this._cache = null
|
|
136
|
+
this._loading = null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load roles from entity (async, must be called before use)
|
|
141
|
+
*/
|
|
142
|
+
async load(): Promise<void> {
|
|
143
|
+
// Prevent concurrent loads
|
|
144
|
+
if (this._loading) {
|
|
145
|
+
return this._loading
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this._loading = this._doLoad()
|
|
149
|
+
await this._loading
|
|
150
|
+
this._loading = null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Internal load implementation
|
|
155
|
+
*/
|
|
156
|
+
private async _doLoad(): Promise<void> {
|
|
157
|
+
const manager = this._orchestrator?.get(this._entityName)
|
|
158
|
+
if (!manager) {
|
|
159
|
+
console.warn(`[EntityRolesProvider] Entity '${this._entityName}' not found`)
|
|
160
|
+
this._cache = { permissions: {}, hierarchy: {}, labels: {} }
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const { items: roles } = await manager.list({ page_size: 1000 })
|
|
166
|
+
|
|
167
|
+
// Build cache
|
|
168
|
+
const permissions: Record<string, string[]> = {}
|
|
169
|
+
const hierarchy: Record<string, string[]> = {}
|
|
170
|
+
const labels: Record<string, string> = {}
|
|
171
|
+
|
|
172
|
+
for (const role of roles) {
|
|
173
|
+
const name = role[this._nameField] as string
|
|
174
|
+
if (!name) continue
|
|
175
|
+
|
|
176
|
+
permissions[name] = (role[this._permissionsField] as string[]) || []
|
|
177
|
+
labels[name] = (role[this._labelField] as string) || name
|
|
178
|
+
|
|
179
|
+
const inherits = role[this._inheritsField] as string[] | undefined
|
|
180
|
+
if (inherits && inherits.length > 0) {
|
|
181
|
+
hierarchy[name] = inherits
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this._cache = { permissions, hierarchy, labels }
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`[EntityRolesProvider] Failed to load roles:`, error)
|
|
188
|
+
this._cache = { permissions: {}, hierarchy: {}, labels: {} }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get cache, throwing if not loaded
|
|
194
|
+
*/
|
|
195
|
+
private _getCache(): RoleCache {
|
|
196
|
+
if (!this._cache) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
'[EntityRolesProvider] Roles not loaded. Call load() before using the adapter.'
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
return this._cache
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get permissions for a role
|
|
206
|
+
*/
|
|
207
|
+
getPermissions(role: string): string[] {
|
|
208
|
+
const cache = this._getCache()
|
|
209
|
+
return cache.permissions[role] || []
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get all defined roles
|
|
214
|
+
*/
|
|
215
|
+
getRoles(): string[] {
|
|
216
|
+
const cache = this._getCache()
|
|
217
|
+
return Object.keys(cache.permissions)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get role hierarchy
|
|
222
|
+
*/
|
|
223
|
+
getHierarchy(): RoleHierarchyMap {
|
|
224
|
+
const cache = this._getCache()
|
|
225
|
+
return cache.hierarchy
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get role metadata
|
|
230
|
+
*/
|
|
231
|
+
getRoleMeta(role: string): RoleMeta | null {
|
|
232
|
+
const cache = this._getCache()
|
|
233
|
+
const label = cache.labels[role]
|
|
234
|
+
if (!label) return null
|
|
235
|
+
return { label }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Invalidate cache (triggers reload on next access)
|
|
240
|
+
* Call this after role changes
|
|
241
|
+
*/
|
|
242
|
+
invalidate(): void {
|
|
243
|
+
this._cache = null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if cache is loaded
|
|
248
|
+
*/
|
|
249
|
+
get isLoaded(): boolean {
|
|
250
|
+
return this._cache !== null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Entity-backed adapter can always persist (via entity manager)
|
|
255
|
+
*/
|
|
256
|
+
get canPersist(): boolean {
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
261
|
+
// Role query methods
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if a role exists
|
|
266
|
+
*/
|
|
267
|
+
roleExists(role: string): boolean {
|
|
268
|
+
const cache = this._getCache()
|
|
269
|
+
return cache.permissions[role] !== undefined
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get complete role object
|
|
274
|
+
*/
|
|
275
|
+
getRole(role: string): RoleData | null {
|
|
276
|
+
const cache = this._getCache()
|
|
277
|
+
if (cache.permissions[role] === undefined) {
|
|
278
|
+
return null
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
name: role,
|
|
282
|
+
label: cache.labels[role] || role,
|
|
283
|
+
permissions: cache.permissions[role] || [],
|
|
284
|
+
inherits: cache.hierarchy[role] || [],
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
289
|
+
// Mutation methods (via entity manager)
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get the entity manager for roles
|
|
294
|
+
*/
|
|
295
|
+
private _getManager(): EntityManager {
|
|
296
|
+
const manager = this._orchestrator?.get(this._entityName)
|
|
297
|
+
if (!manager) {
|
|
298
|
+
throw new Error(`Entity '${this._entityName}' not found`)
|
|
299
|
+
}
|
|
300
|
+
return manager
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Create a new role
|
|
305
|
+
*/
|
|
306
|
+
async createRole(
|
|
307
|
+
name: string,
|
|
308
|
+
{ label, permissions = [], inherits = [] }: { label?: string; permissions?: string[]; inherits?: string[] } = {}
|
|
309
|
+
): Promise<RoleEntity> {
|
|
310
|
+
if (this.roleExists(name)) {
|
|
311
|
+
throw new Error(`Role '${name}' already exists`)
|
|
312
|
+
}
|
|
313
|
+
const manager = this._getManager()
|
|
314
|
+
const data = {
|
|
315
|
+
[this._nameField]: name,
|
|
316
|
+
[this._labelField]: label || name,
|
|
317
|
+
[this._permissionsField]: permissions,
|
|
318
|
+
[this._inheritsField]: inherits,
|
|
319
|
+
}
|
|
320
|
+
const result = await manager.create(data)
|
|
321
|
+
this.invalidate()
|
|
322
|
+
return result
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Update an existing role
|
|
327
|
+
*/
|
|
328
|
+
async updateRole(
|
|
329
|
+
name: string,
|
|
330
|
+
{ label, permissions, inherits }: { label?: string; permissions?: string[]; inherits?: string[] } = {}
|
|
331
|
+
): Promise<RoleEntity> {
|
|
332
|
+
// Ensure loaded first
|
|
333
|
+
this._getCache()
|
|
334
|
+
const manager = this._getManager()
|
|
335
|
+
|
|
336
|
+
// Find the role entity by name
|
|
337
|
+
const response = await manager.list({
|
|
338
|
+
filter: { [this._nameField]: name },
|
|
339
|
+
limit: 1,
|
|
340
|
+
})
|
|
341
|
+
const roles = response.data || response.items
|
|
342
|
+
if (roles.length === 0) {
|
|
343
|
+
throw new Error(`Role '${name}' does not exist`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const roleEntity = roles[0]
|
|
347
|
+
if (!roleEntity) {
|
|
348
|
+
throw new Error(`Role '${name}' does not exist`)
|
|
349
|
+
}
|
|
350
|
+
const updates: Record<string, unknown> = {}
|
|
351
|
+
if (label !== undefined) updates[this._labelField] = label
|
|
352
|
+
if (permissions !== undefined) updates[this._permissionsField] = permissions
|
|
353
|
+
if (inherits !== undefined) updates[this._inheritsField] = inherits
|
|
354
|
+
|
|
355
|
+
const result = await manager.update(roleEntity.id as string | number, updates)
|
|
356
|
+
this.invalidate()
|
|
357
|
+
return result
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Delete a role
|
|
362
|
+
*/
|
|
363
|
+
async deleteRole(name: string): Promise<void> {
|
|
364
|
+
// Ensure loaded first
|
|
365
|
+
this._getCache()
|
|
366
|
+
const manager = this._getManager()
|
|
367
|
+
|
|
368
|
+
// Find the role entity by name
|
|
369
|
+
const response = await manager.list({
|
|
370
|
+
filter: { [this._nameField]: name },
|
|
371
|
+
limit: 1,
|
|
372
|
+
})
|
|
373
|
+
const roles = response.data || response.items
|
|
374
|
+
if (roles.length === 0) {
|
|
375
|
+
throw new Error(`Role '${name}' does not exist`)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const roleToDelete = roles[0]
|
|
379
|
+
if (!roleToDelete) {
|
|
380
|
+
throw new Error(`Role '${name}' does not exist`)
|
|
381
|
+
}
|
|
382
|
+
await manager.delete(roleToDelete.id as string | number)
|
|
383
|
+
this.invalidate()
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionMatcher - Wildcard permission matching
|
|
3
|
+
*
|
|
4
|
+
* Supports two wildcard patterns (similar to signals):
|
|
5
|
+
* - `*` matches exactly one segment
|
|
6
|
+
* - `**` matches zero or more segments (greedy)
|
|
7
|
+
*
|
|
8
|
+
* Permission format: namespace:target:action
|
|
9
|
+
* Examples:
|
|
10
|
+
* entity:books:read - specific permission
|
|
11
|
+
* entity:books:* - any action on books
|
|
12
|
+
* entity:*:read - read on any entity
|
|
13
|
+
* entity:** - all entity permissions
|
|
14
|
+
* ** - super admin (matches everything)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* PermissionMatcher.matches('entity:*:read', 'entity:books:read') // true
|
|
18
|
+
* PermissionMatcher.matches('entity:**', 'entity:books:read') // true
|
|
19
|
+
* PermissionMatcher.matches('**', 'anything:here') // true
|
|
20
|
+
*/
|
|
21
|
+
export class PermissionMatcher {
|
|
22
|
+
/**
|
|
23
|
+
* Check if a permission pattern matches a required permission
|
|
24
|
+
*
|
|
25
|
+
* @param pattern - Pattern with wildcards (e.g., 'entity:*:read')
|
|
26
|
+
* @param required - Required permission (e.g., 'entity:books:read')
|
|
27
|
+
* @returns True if pattern matches required
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* PermissionMatcher.matches('entity:books:read', 'entity:books:read') // true (exact)
|
|
31
|
+
* PermissionMatcher.matches('entity:books:*', 'entity:books:read') // true (* = one segment)
|
|
32
|
+
* PermissionMatcher.matches('entity:*:read', 'entity:books:read') // true
|
|
33
|
+
* PermissionMatcher.matches('entity:**', 'entity:books:read') // true (** = greedy)
|
|
34
|
+
* PermissionMatcher.matches('**', 'entity:books:read') // true (super admin)
|
|
35
|
+
* PermissionMatcher.matches('entity:*:read', 'entity:a:b:read') // false (* = exactly one)
|
|
36
|
+
*/
|
|
37
|
+
static matches(pattern: string, required: string): boolean {
|
|
38
|
+
// Super admin
|
|
39
|
+
if (pattern === '**') return true
|
|
40
|
+
|
|
41
|
+
// Exact match
|
|
42
|
+
if (pattern === required) return true
|
|
43
|
+
|
|
44
|
+
const patternParts = pattern.split(':')
|
|
45
|
+
const requiredParts = required.split(':')
|
|
46
|
+
|
|
47
|
+
return this._matchParts(patternParts, requiredParts, 0, 0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Recursive matching of pattern parts against required parts
|
|
52
|
+
*/
|
|
53
|
+
private static _matchParts(
|
|
54
|
+
pattern: string[],
|
|
55
|
+
required: string[],
|
|
56
|
+
pi: number,
|
|
57
|
+
ri: number
|
|
58
|
+
): boolean {
|
|
59
|
+
// Both exhausted = match
|
|
60
|
+
if (pi === pattern.length && ri === required.length) {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Pattern exhausted but required has more = no match
|
|
65
|
+
if (pi === pattern.length) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const p = pattern[pi]
|
|
70
|
+
|
|
71
|
+
// ** matches zero or more remaining segments
|
|
72
|
+
if (p === '**') {
|
|
73
|
+
// ** at end matches everything remaining
|
|
74
|
+
if (pi === pattern.length - 1) {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try matching ** against 0, 1, 2, ... segments
|
|
79
|
+
for (let skip = 0; skip <= required.length - ri; skip++) {
|
|
80
|
+
if (this._matchParts(pattern, required, pi + 1, ri + skip)) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Required exhausted but pattern has more (and not **)
|
|
88
|
+
if (ri === required.length) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// * matches exactly one segment
|
|
93
|
+
if (p === '*') {
|
|
94
|
+
return this._matchParts(pattern, required, pi + 1, ri + 1)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Literal match
|
|
98
|
+
if (p === required[ri]) {
|
|
99
|
+
return this._matchParts(pattern, required, pi + 1, ri + 1)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if any pattern in array matches the required permission
|
|
107
|
+
*
|
|
108
|
+
* @param patterns - Array of patterns (user's permissions)
|
|
109
|
+
* @param required - Required permission to check
|
|
110
|
+
* @returns True if any pattern matches
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* const userPerms = ['entity:*:read', 'entity:*:list', 'auth:impersonate']
|
|
114
|
+
* PermissionMatcher.any(userPerms, 'entity:books:read') // true
|
|
115
|
+
* PermissionMatcher.any(userPerms, 'entity:books:delete') // false
|
|
116
|
+
*/
|
|
117
|
+
static any(patterns: string[] | null | undefined, required: string): boolean {
|
|
118
|
+
if (!patterns || patterns.length === 0) return false
|
|
119
|
+
return patterns.some((p) => this.matches(p, required))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Filter permissions that match a pattern
|
|
124
|
+
*
|
|
125
|
+
* @param permissions - Array of specific permissions
|
|
126
|
+
* @param pattern - Pattern to filter by
|
|
127
|
+
* @returns Permissions that match the pattern
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* const allPerms = ['entity:books:read', 'entity:books:create', 'auth:login']
|
|
131
|
+
* PermissionMatcher.filter(allPerms, 'entity:books:*') // ['entity:books:read', 'entity:books:create']
|
|
132
|
+
*/
|
|
133
|
+
static filter(permissions: string[], pattern: string): string[] {
|
|
134
|
+
return permissions.filter((p) => this.matches(pattern, p))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Expand a pattern against registered permissions
|
|
139
|
+
* Useful for UI: show what a wildcard pattern actually grants
|
|
140
|
+
*
|
|
141
|
+
* @param pattern - Pattern to expand
|
|
142
|
+
* @param allPermissions - All registered permissions
|
|
143
|
+
* @returns Permissions that the pattern would grant
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* const registry = ['entity:books:read', 'entity:books:create', 'entity:loans:read']
|
|
147
|
+
* PermissionMatcher.expand('entity:*:read', registry) // ['entity:books:read', 'entity:loans:read']
|
|
148
|
+
*/
|
|
149
|
+
static expand(pattern: string, allPermissions: string[]): string[] {
|
|
150
|
+
return allPermissions.filter((p) => this.matches(pattern, p))
|
|
151
|
+
}
|
|
152
|
+
}
|