@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,584 @@
1
+ /**
2
+ * PersistableRoleProvider - Roles provider with load/persist callbacks
3
+ *
4
+ * Flexible adapter that can load role→permissions mapping from any source
5
+ * (localStorage, API, entity) and persist changes back.
6
+ *
7
+ * Features:
8
+ * - Fixed permissions (incompressible, always applied, never overridden)
9
+ * - Default mapping as fallback
10
+ * - Async load/persist callbacks
11
+ * - Merge strategy for defaults + loaded data
12
+ * - Cache invalidation on persist
13
+ *
14
+ * Permission priority (highest to lowest):
15
+ * 1. fixed - System permissions, always present, never overridden
16
+ * 2. loaded - Data from load() callback
17
+ * 3. defaults - Fallback when load returns null/fails
18
+ *
19
+ * @example
20
+ * // With fixed system permissions + API loading
21
+ * const adapter = new PersistableRoleProvider({
22
+ * // Fixed: ALWAYS present, even if API returns different data
23
+ * fixed: {
24
+ * role_permissions: {
25
+ * ROLE_ANONYMOUS: ['auth:login', 'auth:register'],
26
+ * ROLE_USER: ['auth:authenticated', 'auth:logout', 'profile:read']
27
+ * }
28
+ * },
29
+ * // Defaults: used before load() or if load fails
30
+ * defaults: {
31
+ * role_permissions: {
32
+ * ROLE_USER: ['entity:*:read'],
33
+ * ROLE_ADMIN: ['entity:**']
34
+ * }
35
+ * },
36
+ * // Load from API (requires auth, called after login)
37
+ * load: async () => {
38
+ * const res = await fetch('/api/config/roles')
39
+ * return res.json()
40
+ * },
41
+ * autoLoad: false // Load manually after auth
42
+ * })
43
+ *
44
+ * // After user authenticates
45
+ * signals.on('auth:login', () => adapter.load())
46
+ *
47
+ * @example
48
+ * // With localStorage
49
+ * const adapter = new PersistableRoleProvider({
50
+ * load: () => JSON.parse(localStorage.getItem('roles') || 'null'),
51
+ * persist: (data) => localStorage.setItem('roles', JSON.stringify(data)),
52
+ * defaults: {
53
+ * role_permissions: {
54
+ * ROLE_USER: ['entity:*:read', 'entity:*:list'],
55
+ * ROLE_ADMIN: ['entity:**', 'admin:**']
56
+ * }
57
+ * }
58
+ * })
59
+ */
60
+
61
+ import { RoleProvider } from './RolesProvider'
62
+ import type { RoleData, RoleHierarchyMap } from './RolesProvider'
63
+
64
+ /**
65
+ * Role configuration structure
66
+ */
67
+ export interface RoleConfig {
68
+ role_hierarchy?: Record<string, string[]>
69
+ role_permissions?: Record<string, string[]>
70
+ role_labels?: Record<string, string>
71
+ }
72
+
73
+ /**
74
+ * Merge strategy for combining defaults with loaded data
75
+ */
76
+ export type MergeStrategy = 'extend' | 'replace' | 'defaults-only'
77
+
78
+ /**
79
+ * Load callback type
80
+ */
81
+ export type LoadCallback = () => Promise<RoleConfig | null> | RoleConfig | null
82
+
83
+ /**
84
+ * Persist callback type
85
+ */
86
+ export type PersistCallback = (data: RoleConfig) => Promise<void> | void
87
+
88
+ /**
89
+ * Options for PersistableRoleProvider
90
+ */
91
+ export interface PersistableRolesProviderOptions {
92
+ load?: LoadCallback
93
+ persist?: PersistCallback
94
+ fixed?: RoleConfig
95
+ defaults?: RoleConfig
96
+ mergeStrategy?: MergeStrategy
97
+ autoLoad?: boolean
98
+ }
99
+
100
+ /**
101
+ * Options for createLocalStorageRolesProvider
102
+ */
103
+ export interface LocalStorageRolesProviderOptions {
104
+ key?: string
105
+ fixed?: RoleConfig
106
+ defaults?: RoleConfig
107
+ mergeStrategy?: MergeStrategy
108
+ }
109
+
110
+ export class PersistableRoleProvider extends RoleProvider {
111
+ private _loadFn: LoadCallback | null
112
+ private _persistFn: PersistCallback | null
113
+ private _mergeStrategy: MergeStrategy
114
+ private _autoLoad: boolean
115
+
116
+ // Fixed (incompressible, always applied on top)
117
+ private _fixed: Required<RoleConfig>
118
+
119
+ // Defaults (fallback)
120
+ private _defaults: Required<RoleConfig>
121
+
122
+ // Current state (starts with defaults)
123
+ protected _hierarchy: Record<string, string[]>
124
+ protected _permissions: Record<string, string[]>
125
+ protected _labels: Record<string, string>
126
+
127
+ // Loading state
128
+ // Note: _loaded is public to allow createLocalStorageRolesProvider to set it
129
+ public _loaded: boolean = false
130
+ private _loading: Promise<void> | null = null
131
+ private _dirty: boolean = false
132
+
133
+ /**
134
+ * Create a persistable roles provider
135
+ */
136
+ constructor(options: PersistableRolesProviderOptions = {}) {
137
+ super()
138
+
139
+ this._loadFn = options.load || null
140
+ this._persistFn = options.persist || null
141
+ this._mergeStrategy = options.mergeStrategy || 'extend'
142
+ this._autoLoad = options.autoLoad ?? true
143
+
144
+ // Fixed (incompressible, always applied on top)
145
+ this._fixed = {
146
+ role_hierarchy: options.fixed?.role_hierarchy || {},
147
+ role_permissions: options.fixed?.role_permissions || {},
148
+ role_labels: options.fixed?.role_labels || {},
149
+ }
150
+
151
+ // Defaults (fallback)
152
+ this._defaults = {
153
+ role_hierarchy: options.defaults?.role_hierarchy || {},
154
+ role_permissions: options.defaults?.role_permissions || {},
155
+ role_labels: options.defaults?.role_labels || {},
156
+ }
157
+
158
+ // Current state (starts with defaults)
159
+ this._hierarchy = { ...this._defaults.role_hierarchy }
160
+ this._permissions = { ...this._defaults.role_permissions }
161
+ this._labels = { ...this._defaults.role_labels }
162
+ }
163
+
164
+ /**
165
+ * Load configuration from source
166
+ */
167
+ async load(): Promise<void> {
168
+ if (!this._loadFn) {
169
+ this._loaded = true
170
+ return
171
+ }
172
+
173
+ // Prevent concurrent loads
174
+ if (this._loading) {
175
+ return this._loading
176
+ }
177
+
178
+ this._loading = this._doLoad()
179
+ try {
180
+ await this._loading
181
+ } finally {
182
+ this._loading = null
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Internal load implementation
188
+ */
189
+ private async _doLoad(): Promise<void> {
190
+ try {
191
+ const data = await this._loadFn!()
192
+
193
+ if (data) {
194
+ this._applyData(data)
195
+ }
196
+
197
+ this._loaded = true
198
+ this._dirty = false
199
+ } catch (err) {
200
+ console.error('[PersistableRoleProvider] Load failed:', err)
201
+ // Keep defaults on error
202
+ this._loaded = true
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Apply loaded data according to merge strategy
208
+ */
209
+ _applyData(data: RoleConfig): void {
210
+ switch (this._mergeStrategy) {
211
+ case 'replace':
212
+ // Loaded data replaces everything
213
+ this._hierarchy = data.role_hierarchy || {}
214
+ this._permissions = data.role_permissions || {}
215
+ this._labels = data.role_labels || {}
216
+ break
217
+
218
+ case 'defaults-only':
219
+ // Ignore loaded data, use defaults
220
+ break
221
+
222
+ case 'extend':
223
+ default:
224
+ // Loaded extends defaults (loaded takes priority)
225
+ this._hierarchy = {
226
+ ...this._defaults.role_hierarchy,
227
+ ...(data.role_hierarchy || {}),
228
+ }
229
+ this._permissions = {
230
+ ...this._defaults.role_permissions,
231
+ ...(data.role_permissions || {}),
232
+ }
233
+ this._labels = {
234
+ ...this._defaults.role_labels,
235
+ ...(data.role_labels || {}),
236
+ }
237
+ break
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Persist current configuration to source
243
+ */
244
+ async persist(): Promise<void> {
245
+ if (!this._persistFn) {
246
+ console.warn('[PersistableRoleProvider] No persist function configured')
247
+ return
248
+ }
249
+
250
+ const data: RoleConfig = {
251
+ role_hierarchy: this._hierarchy,
252
+ role_permissions: this._permissions,
253
+ role_labels: this._labels,
254
+ }
255
+
256
+ try {
257
+ await this._persistFn(data)
258
+ this._dirty = false
259
+ } catch (err) {
260
+ console.error('[PersistableRoleProvider] Persist failed:', err)
261
+ throw err
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Ensure data is loaded (auto-load if needed)
267
+ */
268
+ private async _ensureLoaded(): Promise<void> {
269
+ if (!this._loaded && this._autoLoad) {
270
+ await this.load()
271
+ }
272
+ }
273
+
274
+ // ─────────────────────────────────────────────────────────────────────────────
275
+ // RoleProvider interface
276
+ // ─────────────────────────────────────────────────────────────────────────────
277
+
278
+ /**
279
+ * Get permissions for a role
280
+ *
281
+ * Merges: current permissions + fixed permissions (fixed always wins)
282
+ */
283
+ getPermissions(role: string): string[] {
284
+ // Note: This is synchronous. For async sources, call load() first
285
+ // or use autoLoad + await ensureReady() before first use
286
+ const base = this._permissions[role] || []
287
+ const fixed = this._fixed.role_permissions[role] || []
288
+
289
+ // Merge: base + fixed (deduplicated)
290
+ if (fixed.length === 0) return base
291
+ if (base.length === 0) return fixed
292
+
293
+ const merged = [...base]
294
+ for (const perm of fixed) {
295
+ if (!merged.includes(perm)) {
296
+ merged.push(perm)
297
+ }
298
+ }
299
+ return merged
300
+ }
301
+
302
+ /**
303
+ * Get all defined roles
304
+ *
305
+ * Includes roles from: current + fixed
306
+ */
307
+ getRoles(): string[] {
308
+ const roles = new Set<string>([
309
+ ...Object.keys(this._permissions),
310
+ ...Object.keys(this._fixed.role_permissions),
311
+ ])
312
+ return [...roles]
313
+ }
314
+
315
+ /**
316
+ * Get role hierarchy
317
+ *
318
+ * Merges: current hierarchy + fixed hierarchy
319
+ */
320
+ getHierarchy(): RoleHierarchyMap {
321
+ return {
322
+ ...this._hierarchy,
323
+ ...this._fixed.role_hierarchy,
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Get role labels for display
329
+ *
330
+ * Merges: current labels + fixed labels
331
+ */
332
+ getLabels(): Record<string, string> {
333
+ return {
334
+ ...this._labels,
335
+ ...this._fixed.role_labels,
336
+ }
337
+ }
338
+
339
+ // getAnonymousRole() inherited from RoleProvider (returns 'ROLE_ANONYMOUS')
340
+
341
+ // ─────────────────────────────────────────────────────────────────────────────
342
+ // Role query methods
343
+ // ─────────────────────────────────────────────────────────────────────────────
344
+
345
+ /**
346
+ * Check if a role exists (in current or fixed)
347
+ */
348
+ roleExists(role: string): boolean {
349
+ return (
350
+ this._permissions[role] !== undefined || this._fixed.role_permissions[role] !== undefined
351
+ )
352
+ }
353
+
354
+ /**
355
+ * Get complete role object
356
+ */
357
+ getRole(role: string): RoleData | null {
358
+ if (!this.roleExists(role)) {
359
+ return null
360
+ }
361
+ return {
362
+ name: role,
363
+ label: this._labels[role] || this._fixed.role_labels[role] || role,
364
+ permissions: this.getPermissions(role),
365
+ inherits: this._hierarchy[role] || this._fixed.role_hierarchy[role] || [],
366
+ }
367
+ }
368
+
369
+ // ─────────────────────────────────────────────────────────────────────────────
370
+ // Mutation methods
371
+ // ─────────────────────────────────────────────────────────────────────────────
372
+
373
+ /**
374
+ * Create a new role (async, auto-persists)
375
+ */
376
+ async createRole(
377
+ name: string,
378
+ { label, permissions = [], inherits = [] }: { label?: string; permissions?: string[]; inherits?: string[] } = {}
379
+ ): Promise<RoleData> {
380
+ if (this.roleExists(name)) {
381
+ throw new Error(`Role '${name}' already exists`)
382
+ }
383
+ this._permissions[name] = [...permissions]
384
+ if (label) this._labels[name] = label
385
+ if (inherits.length > 0) this._hierarchy[name] = [...inherits]
386
+ this._dirty = true
387
+
388
+ if (this._persistFn) {
389
+ await this.persist()
390
+ }
391
+
392
+ return this.getRole(name)!
393
+ }
394
+
395
+ /**
396
+ * Update an existing role (async, auto-persists)
397
+ */
398
+ async updateRole(
399
+ name: string,
400
+ { label, permissions, inherits }: { label?: string; permissions?: string[]; inherits?: string[] } = {}
401
+ ): Promise<RoleData> {
402
+ if (!this.roleExists(name)) {
403
+ throw new Error(`Role '${name}' does not exist`)
404
+ }
405
+ if (permissions !== undefined) this._permissions[name] = [...permissions]
406
+ if (label !== undefined) this._labels[name] = label
407
+ if (inherits !== undefined) this._hierarchy[name] = [...inherits]
408
+ this._dirty = true
409
+
410
+ if (this._persistFn) {
411
+ await this.persist()
412
+ }
413
+
414
+ return this.getRole(name)!
415
+ }
416
+
417
+ /**
418
+ * Set permissions for a role
419
+ */
420
+ setRolePermissions(role: string, permissions: string[]): this {
421
+ this._permissions[role] = [...permissions]
422
+ this._dirty = true
423
+ return this
424
+ }
425
+
426
+ /**
427
+ * Add permissions to a role
428
+ */
429
+ addRolePermissions(role: string, permissions: string[]): this {
430
+ const current = this._permissions[role] || []
431
+ const newPerms = permissions.filter((p) => !current.includes(p))
432
+ this._permissions[role] = [...current, ...newPerms]
433
+ this._dirty = true
434
+ return this
435
+ }
436
+
437
+ /**
438
+ * Remove permissions from a role
439
+ */
440
+ removeRolePermissions(role: string, permissions: string[]): this {
441
+ const current = this._permissions[role] || []
442
+ this._permissions[role] = current.filter((p) => !permissions.includes(p))
443
+ this._dirty = true
444
+ return this
445
+ }
446
+
447
+ /**
448
+ * Set role hierarchy
449
+ */
450
+ setRoleHierarchy(role: string, inherits: string[]): this {
451
+ this._hierarchy[role] = [...inherits]
452
+ this._dirty = true
453
+ return this
454
+ }
455
+
456
+ /**
457
+ * Set role label
458
+ */
459
+ setRoleLabel(role: string, label: string): this {
460
+ this._labels[role] = label
461
+ this._dirty = true
462
+ return this
463
+ }
464
+
465
+ /**
466
+ * Delete a role entirely (async, auto-persists)
467
+ */
468
+ async deleteRole(name: string): Promise<void> {
469
+ if (!this.roleExists(name)) {
470
+ throw new Error(`Role '${name}' does not exist`)
471
+ }
472
+ delete this._permissions[name]
473
+ delete this._hierarchy[name]
474
+ delete this._labels[name]
475
+ this._dirty = true
476
+
477
+ if (this._persistFn) {
478
+ await this.persist()
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Reset to defaults
484
+ */
485
+ reset(): this {
486
+ this._hierarchy = { ...this._defaults.role_hierarchy }
487
+ this._permissions = { ...this._defaults.role_permissions }
488
+ this._labels = { ...this._defaults.role_labels }
489
+ this._dirty = true
490
+ return this
491
+ }
492
+
493
+ // ─────────────────────────────────────────────────────────────────────────────
494
+ // State inspection
495
+ // ─────────────────────────────────────────────────────────────────────────────
496
+
497
+ /**
498
+ * Check if data has been loaded
499
+ */
500
+ get isLoaded(): boolean {
501
+ return this._loaded
502
+ }
503
+
504
+ /**
505
+ * Check if there are unsaved changes
506
+ */
507
+ get isDirty(): boolean {
508
+ return this._dirty
509
+ }
510
+
511
+ /**
512
+ * Check if adapter supports persistence
513
+ * Returns true if a persist function was provided
514
+ */
515
+ get canPersist(): boolean {
516
+ return !!this._persistFn
517
+ }
518
+
519
+ /**
520
+ * Get current configuration as object
521
+ */
522
+ toJSON(): RoleConfig {
523
+ return {
524
+ role_hierarchy: this._hierarchy,
525
+ role_permissions: this._permissions,
526
+ role_labels: this._labels,
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Get a ready promise (resolves when loaded)
532
+ */
533
+ async ensureReady(): Promise<this> {
534
+ await this._ensureLoaded()
535
+ return this
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Create a localStorage-backed roles provider
541
+ *
542
+ * Unlike async sources, localStorage is synchronous so data is loaded
543
+ * immediately at construction time. No need to call ensureReady().
544
+ */
545
+ export function createLocalStorageRolesProvider(
546
+ options: LocalStorageRolesProviderOptions = {}
547
+ ): PersistableRoleProvider {
548
+ const key = options.key || 'qdadm_roles'
549
+
550
+ // Load data synchronously from localStorage (localStorage is sync)
551
+ let initialData: RoleConfig | null = null
552
+ try {
553
+ const stored = localStorage.getItem(key)
554
+ initialData = stored ? JSON.parse(stored) : null
555
+ } catch {
556
+ // Ignore parse errors, will use defaults
557
+ }
558
+
559
+ const adapter = new PersistableRoleProvider({
560
+ load: () => {
561
+ try {
562
+ const stored = localStorage.getItem(key)
563
+ return stored ? JSON.parse(stored) : null
564
+ } catch {
565
+ return null
566
+ }
567
+ },
568
+ persist: (data) => {
569
+ localStorage.setItem(key, JSON.stringify(data))
570
+ },
571
+ fixed: options.fixed,
572
+ defaults: options.defaults,
573
+ mergeStrategy: options.mergeStrategy,
574
+ autoLoad: false, // We load synchronously below
575
+ })
576
+
577
+ // Apply initial data immediately (synchronous load)
578
+ if (initialData) {
579
+ adapter._applyData(initialData)
580
+ }
581
+ adapter._loaded = true
582
+
583
+ return adapter
584
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * RolesManager - System entity manager for roles
3
+ *
4
+ * Uses RoleProviderStorage as bridge to rolesProvider.
5
+ * Allows SecurityModule to use standard ctx.crud() pattern.
6
+ */
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ import { EntityManager } from '../entity/EntityManager'
10
+ import { RoleProviderStorage } from './RolesProviderStorage'
11
+ import type { RoleProvider } from './RolesProvider'
12
+ import type { PermissionRegistry } from './PermissionRegistry'
13
+ import type { IStorage, EntityRecord } from '../types'
14
+
15
+ /**
16
+ * RoleData as EntityRecord (with id mapped to name)
17
+ */
18
+ interface RoleRecord extends EntityRecord {
19
+ name: string
20
+ label: string
21
+ permissions: string[]
22
+ inherits: string[]
23
+ }
24
+
25
+ /**
26
+ * Options for RolesManager
27
+ */
28
+ export interface RolesManagerOptions {
29
+ rolesProvider?: RoleProvider | null
30
+ permissionRegistry?: PermissionRegistry | null
31
+ adminPermission?: string
32
+ [key: string]: unknown
33
+ }
34
+
35
+ // Type for EntityManager base class
36
+ interface EntityManagerInstance {
37
+ _hasSecurityChecker(): boolean
38
+ authAdapter: {
39
+ isGranted(permission: string): boolean
40
+ }
41
+ }
42
+
43
+ export class RolesManager extends EntityManager<RoleRecord> {
44
+ private _rolesProvider: RoleProvider | null
45
+ private _permissionRegistry: PermissionRegistry | null
46
+ private _adminPermission: string
47
+
48
+ /**
49
+ * Protected system roles that cannot be deleted
50
+ */
51
+ static PROTECTED_ROLES = ['ROLE_ANONYMOUS']
52
+
53
+ constructor(options: RolesManagerOptions = {}) {
54
+ const {
55
+ rolesProvider = null,
56
+ permissionRegistry = null,
57
+ adminPermission = 'security:roles:manage',
58
+ ...rest
59
+ } = options
60
+
61
+ const storage = new RoleProviderStorage(rolesProvider) as unknown as IStorage<RoleRecord>
62
+
63
+ super({
64
+ name: 'roles',
65
+ labelField: 'label',
66
+ idField: 'name',
67
+ system: true, // System-provided entity
68
+ fields: {
69
+ name: { type: 'text', label: 'Role Name', required: true },
70
+ label: { type: 'text', label: 'Display Label' },
71
+ permissions: { type: 'array', label: 'Permissions', default: [] },
72
+ inherits: { type: 'array', label: 'Inherits From', default: [] },
73
+ },
74
+ storage,
75
+ ...rest,
76
+ })
77
+
78
+ this._rolesProvider = rolesProvider
79
+ this._permissionRegistry = permissionRegistry
80
+ this._adminPermission = adminPermission
81
+ }
82
+
83
+ /**
84
+ * Get rolesProvider (for RoleForm permission picker)
85
+ */
86
+ get rolesProvider(): RoleProvider | null {
87
+ return this._rolesProvider
88
+ }
89
+
90
+ /**
91
+ * Get permission registry (for RoleForm permission picker)
92
+ */
93
+ get permissionRegistry(): PermissionRegistry | null {
94
+ return this._permissionRegistry
95
+ }
96
+
97
+ /**
98
+ * Get admin permission (for external registration)
99
+ */
100
+ get adminPermission(): string {
101
+ return this._adminPermission
102
+ }
103
+
104
+ private _isAdmin(): boolean {
105
+ const self = this as unknown as EntityManagerInstance
106
+ if (self._hasSecurityChecker()) {
107
+ return self.authAdapter.isGranted(this._adminPermission)
108
+ }
109
+ return false
110
+ }
111
+
112
+ canRead(): boolean {
113
+ return this._isAdmin()
114
+ }
115
+
116
+ canCreate(): boolean {
117
+ return this._isAdmin()
118
+ }
119
+
120
+ canUpdate(): boolean {
121
+ return this._isAdmin()
122
+ }
123
+
124
+ /**
125
+ * Check if can delete (general or row-specific)
126
+ * Protected roles cannot be deleted even with permission
127
+ */
128
+ canDelete(item?: RoleRecord | null): boolean {
129
+ if (!this._isAdmin()) return false
130
+ if (item) {
131
+ // Protected system roles cannot be deleted
132
+ return !RolesManager.PROTECTED_ROLES.includes(item.name)
133
+ }
134
+ return true
135
+ }
136
+ }