@ngbplatform/ui 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. package/vite-public-assets.js +68 -0
@@ -0,0 +1,374 @@
1
+ import Keycloak, {
2
+ type KeycloakInitOptions,
3
+ type KeycloakLoginOptions,
4
+ type KeycloakLogoutOptions,
5
+ type KeycloakTokenParsed,
6
+ } from 'keycloak-js'
7
+ import type { AuthSnapshot } from './types'
8
+ import { readAppEnv } from '../env/runtimeConfig'
9
+
10
+ type KeycloakClaims = KeycloakTokenParsed & {
11
+ email?: string
12
+ preferred_username?: string
13
+ given_name?: string
14
+ family_name?: string
15
+ name?: string
16
+ role?: string | string[]
17
+ roles?: string | string[]
18
+ groups?: string[]
19
+ }
20
+
21
+ type AuthListener = (snapshot: AuthSnapshot) => void
22
+
23
+ const keycloak = new Keycloak({
24
+ url: requiredEnv('VITE_KEYCLOAK_URL'),
25
+ realm: requiredEnv('VITE_KEYCLOAK_REALM'),
26
+ clientId: requiredEnv('VITE_KEYCLOAK_CLIENT_ID'),
27
+ })
28
+
29
+ const listeners = new Set<AuthListener>()
30
+ let initialized = false
31
+ let initPromise: Promise<boolean> | null = null
32
+ let refreshPromise: Promise<string | null> | null = null
33
+ let callbacksRegistered = false
34
+
35
+ function requiredEnv(name: string): string {
36
+ const value = readAppEnv(name)
37
+ if (!value) throw new Error(`Missing required env var: ${name}`)
38
+ return value
39
+ }
40
+
41
+ function normalizeUrl(value: string | null | undefined, fallbackPath: string): string {
42
+ const raw = String(value ?? '').trim()
43
+ if (!raw) return new URL(fallbackPath, window.location.origin).toString()
44
+
45
+ const next = new URL(raw, window.location.origin)
46
+ if (next.host === window.location.host && next.protocol !== window.location.protocol) {
47
+ next.protocol = window.location.protocol
48
+ }
49
+
50
+ return next.toString()
51
+ }
52
+
53
+ function resolveAppBaseUrl(): string {
54
+ const rawBase = String(import.meta.env.BASE_URL ?? '/').trim()
55
+ const normalizedBase = rawBase
56
+ ? (rawBase.endsWith('/') ? rawBase : `${rawBase}/`)
57
+ : '/'
58
+
59
+ return new URL(normalizedBase, window.location.origin).toString()
60
+ }
61
+
62
+ function resolveAppPublicUrl(path: string): string {
63
+ const normalizedPath = String(path ?? '').trim().replace(/^\/+/, '')
64
+ return new URL(normalizedPath, resolveAppBaseUrl()).toString()
65
+ }
66
+
67
+ function parseBoolean(value: string | boolean | null | undefined, defaultValue: boolean): boolean {
68
+ if (typeof value === 'boolean') return value
69
+
70
+ const normalized = String(value ?? '').trim().toLowerCase()
71
+ if (!normalized) return defaultValue
72
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') return true
73
+ if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') return false
74
+ return defaultValue
75
+ }
76
+
77
+ function resolveOnLoad(): 'login-required' | 'check-sso' {
78
+ const normalized = (readAppEnv('VITE_KEYCLOAK_ON_LOAD') || 'login-required').toLowerCase()
79
+ return normalized === 'check-sso' ? 'check-sso' : 'login-required'
80
+ }
81
+
82
+ function shouldUseSilentCheckSso(onLoad: 'login-required' | 'check-sso'): boolean {
83
+ if (onLoad !== 'check-sso') return false
84
+ return parseBoolean(readAppEnv('VITE_KEYCLOAK_SILENT_CHECK_SSO_ENABLED'), false)
85
+ }
86
+
87
+ function resolveSilentCheckSsoRedirectUri(): string {
88
+ return normalizeUrl(readAppEnv('VITE_KEYCLOAK_SILENT_CHECK_SSO_REDIRECT_URI'), resolveAppPublicUrl('silent-check-sso.html'))
89
+ }
90
+
91
+ function resolvePostLogoutRedirectUri(): string {
92
+ const explicit = readAppEnv('VITE_KEYCLOAK_POST_LOGOUT_REDIRECT_URL')
93
+ if (explicit) return normalizeUrl(explicit, resolveAppBaseUrl())
94
+ return normalizeUrl(readAppEnv('VITE_KEYCLOAK_REDIRECT_URL'), resolveAppBaseUrl())
95
+ }
96
+
97
+ function resolveAdminConsoleLocalLogoutEndpoints(): string[] {
98
+ const configuredUrls = [
99
+ readAppEnv('VITE_WATCHDOG_URL'),
100
+ readAppEnv('VITE_BACKGROUND_JOB_URL'),
101
+ ]
102
+
103
+ return Array.from(
104
+ new Set(
105
+ configuredUrls
106
+ .map((value) => String(value ?? '').trim())
107
+ .filter((value) => value.length > 0)
108
+ .map((value) => new URL(value, window.location.origin))
109
+ .map((url) => `${url.origin}/account/local-logout`),
110
+ ),
111
+ )
112
+ }
113
+
114
+ async function logoutKnownAdminConsoles(): Promise<void> {
115
+ const endpoints = resolveAdminConsoleLocalLogoutEndpoints()
116
+ if (endpoints.length === 0) return
117
+
118
+ await Promise.allSettled(
119
+ endpoints.map(async (endpoint) => {
120
+ await fetch(endpoint, {
121
+ method: 'POST',
122
+ credentials: 'include',
123
+ mode: 'cors',
124
+ cache: 'no-store',
125
+ keepalive: true,
126
+ headers: {
127
+ Accept: '*/*',
128
+ },
129
+ })
130
+ }),
131
+ )
132
+ }
133
+
134
+ function resolveLoginRedirectUri(targetPath?: string | null): string {
135
+ const target = String(targetPath ?? '').trim()
136
+ if (/^https?:\/\//i.test(target)) return target
137
+ if (target.startsWith('/')) return new URL(target, window.location.origin).toString()
138
+
139
+ const currentUrl = new URL(window.location.href)
140
+ currentUrl.hash = ''
141
+ return currentUrl.toString()
142
+ }
143
+
144
+ function unique(values: Iterable<string | null | undefined>): string[] {
145
+ return Array.from(
146
+ new Set(
147
+ Array.from(values)
148
+ .map((value) => String(value ?? '').trim())
149
+ .filter((value) => value.length > 0),
150
+ ),
151
+ )
152
+ }
153
+
154
+ function readStringArrayClaim(value: unknown): string[] {
155
+ if (Array.isArray(value)) return unique(value.map((entry) => String(entry ?? '')))
156
+ if (typeof value === 'string') return unique(value.split(/[,\s]+/g))
157
+ return []
158
+ }
159
+
160
+ function accessClaims(): KeycloakClaims | null {
161
+ return (keycloak.tokenParsed ?? null) as KeycloakClaims | null
162
+ }
163
+
164
+ function identityClaims(): KeycloakClaims | null {
165
+ const access = accessClaims()
166
+ const identity = (keycloak.idTokenParsed ?? null) as KeycloakClaims | null
167
+ if (!access && !identity) return null
168
+
169
+ return {
170
+ ...(access ?? {}),
171
+ ...(identity ?? {}),
172
+ }
173
+ }
174
+
175
+ function readRealmRoles(claims: KeycloakClaims | null): string[] {
176
+ return unique(claims?.realm_access?.roles ?? [])
177
+ }
178
+
179
+ function readTopLevelRoles(claims: KeycloakClaims | null): string[] {
180
+ return unique([
181
+ ...readStringArrayClaim(claims?.role),
182
+ ...readStringArrayClaim(claims?.roles),
183
+ ])
184
+ }
185
+
186
+ function readResourceRoles(claims: KeycloakClaims | null): Record<string, string[]> {
187
+ const resourceAccess = claims?.resource_access ?? {}
188
+ const entries = Object.entries(resourceAccess).map(([clientId, access]) => [
189
+ clientId,
190
+ unique(access?.roles ?? []),
191
+ ] as const)
192
+
193
+ return Object.fromEntries(entries.filter(([, roles]) => roles.length > 0))
194
+ }
195
+
196
+ function readGroupRoles(claims: KeycloakClaims | null): string[] {
197
+ return unique(
198
+ readStringArrayClaim(claims?.groups)
199
+ .map((group) => group.split('/').filter(Boolean).at(-1) ?? '')
200
+ .filter((group) => group === 'ngb-admin' || group === 'ngb-user'),
201
+ )
202
+ }
203
+
204
+ function resolveDisplayName(claims: KeycloakClaims | null): string | null {
205
+ const explicitName = String(claims?.name ?? '').trim()
206
+ if (explicitName) return explicitName
207
+
208
+ const fullName = [claims?.given_name, claims?.family_name]
209
+ .map((part) => String(part ?? '').trim())
210
+ .filter((part) => part.length > 0)
211
+ .join(' ')
212
+
213
+ if (fullName) return fullName
214
+
215
+ const preferredUsername = String(claims?.preferred_username ?? '').trim()
216
+ if (preferredUsername) return preferredUsername
217
+
218
+ const email = String(claims?.email ?? '').trim()
219
+ return email || null
220
+ }
221
+
222
+ function snapshot(): AuthSnapshot {
223
+ const profile = identityClaims()
224
+ const access = accessClaims()
225
+ const topLevelRoles = readTopLevelRoles(access)
226
+ const realmRoles = readRealmRoles(access)
227
+ const resourceRoles = readResourceRoles(access)
228
+ const groupRoles = readGroupRoles(profile)
229
+ const roles = unique([
230
+ ...topLevelRoles,
231
+ ...realmRoles,
232
+ ...Object.values(resourceRoles).flat(),
233
+ ...groupRoles,
234
+ ])
235
+
236
+ return {
237
+ initialized,
238
+ authenticated: Boolean(keycloak.authenticated),
239
+ token: keycloak.token ?? null,
240
+ subject: keycloak.subject ?? (typeof profile?.sub === 'string' ? profile.sub : null),
241
+ displayName: resolveDisplayName(profile),
242
+ preferredUsername: String(profile?.preferred_username ?? '').trim() || null,
243
+ email: String(profile?.email ?? '').trim() || null,
244
+ realmRoles,
245
+ resourceRoles,
246
+ roles,
247
+ }
248
+ }
249
+
250
+ function notifyListeners(): void {
251
+ const nextSnapshot = snapshot()
252
+ for (const listener of listeners) listener(nextSnapshot)
253
+ }
254
+
255
+ function registerCallbacks(): void {
256
+ if (callbacksRegistered) return
257
+ callbacksRegistered = true
258
+
259
+ keycloak.onReady = () => {
260
+ initialized = true
261
+ notifyListeners()
262
+ }
263
+ keycloak.onAuthSuccess = notifyListeners
264
+ keycloak.onAuthLogout = notifyListeners
265
+ keycloak.onAuthRefreshSuccess = notifyListeners
266
+ keycloak.onAuthRefreshError = notifyListeners
267
+ keycloak.onAuthError = notifyListeners
268
+ keycloak.onTokenExpired = () => {
269
+ void refreshAccessToken(0).catch(() => {
270
+ keycloak.clearToken()
271
+ notifyListeners()
272
+ })
273
+ }
274
+ }
275
+
276
+ export function subscribeAuth(listener: AuthListener): () => void {
277
+ listeners.add(listener)
278
+ listener(snapshot())
279
+ return () => {
280
+ listeners.delete(listener)
281
+ }
282
+ }
283
+
284
+ export function getAuthSnapshot(): AuthSnapshot {
285
+ return snapshot()
286
+ }
287
+
288
+ export async function initializeAuth(): Promise<AuthSnapshot> {
289
+ registerCallbacks()
290
+
291
+ if (!initPromise) {
292
+ const onLoad = resolveOnLoad()
293
+ const initOptions: KeycloakInitOptions = {
294
+ onLoad,
295
+ pkceMethod: 'S256',
296
+ responseMode: 'fragment',
297
+ checkLoginIframe: false,
298
+ }
299
+
300
+ if (shouldUseSilentCheckSso(onLoad)) {
301
+ initOptions.silentCheckSsoRedirectUri = resolveSilentCheckSsoRedirectUri()
302
+ initOptions.silentCheckSsoFallback = false
303
+ }
304
+
305
+ initPromise = keycloak
306
+ .init(initOptions)
307
+ .then((authenticated) => {
308
+ initialized = true
309
+ notifyListeners()
310
+ return authenticated
311
+ })
312
+ .catch((error) => {
313
+ initPromise = null
314
+ throw error
315
+ })
316
+ }
317
+
318
+ await initPromise
319
+ return snapshot()
320
+ }
321
+
322
+ async function refreshAccessToken(minValidity: number): Promise<string | null> {
323
+ if (!initialized) await initializeAuth()
324
+ if (!keycloak.authenticated || !keycloak.token) return null
325
+
326
+ if (!refreshPromise) {
327
+ refreshPromise = keycloak
328
+ .updateToken(minValidity)
329
+ .then(() => {
330
+ notifyListeners()
331
+ return keycloak.token ?? null
332
+ })
333
+ .catch((error) => {
334
+ keycloak.clearToken()
335
+ notifyListeners()
336
+ throw error
337
+ })
338
+ .finally(() => {
339
+ refreshPromise = null
340
+ })
341
+ }
342
+
343
+ return await refreshPromise
344
+ }
345
+
346
+ export async function getAccessToken(minValidity: number = 30): Promise<string | null> {
347
+ return await refreshAccessToken(minValidity)
348
+ }
349
+
350
+ export async function forceRefreshAccessToken(): Promise<string | null> {
351
+ return await refreshAccessToken(-1)
352
+ }
353
+
354
+ export async function loginWithKeycloak(targetPath?: string | null): Promise<void> {
355
+ if (!initialized) await initializeAuth()
356
+
357
+ const options: KeycloakLoginOptions = {
358
+ redirectUri: resolveLoginRedirectUri(targetPath),
359
+ }
360
+
361
+ await keycloak.login(options)
362
+ }
363
+
364
+ export async function logoutFromKeycloak(): Promise<void> {
365
+ if (!initialized) await initializeAuth()
366
+
367
+ await logoutKnownAdminConsoles()
368
+
369
+ const options: KeycloakLogoutOptions = {
370
+ redirectUri: resolvePostLogoutRedirectUri(),
371
+ }
372
+
373
+ await keycloak.logout(options)
374
+ }
@@ -0,0 +1,35 @@
1
+ import type { NavigationGuardWithThis } from 'vue-router'
2
+
3
+ export type AuthGuardStore = {
4
+ redirecting: boolean
5
+ initialized: boolean
6
+ initializing: boolean
7
+ authenticated: boolean
8
+ error: string | null
9
+ initialize(): Promise<void>
10
+ login(targetPath?: string | null): Promise<void>
11
+ }
12
+
13
+ export function createAuthGuard(resolveAuthStore: () => AuthGuardStore): NavigationGuardWithThis<undefined> {
14
+ return async (to) => {
15
+ if (to.meta?.public === true) return true
16
+
17
+ const auth = resolveAuthStore()
18
+ if (auth.redirecting) return false
19
+
20
+ if (!auth.initialized && !auth.initializing) {
21
+ try {
22
+ await auth.initialize()
23
+ } catch {
24
+ return true
25
+ }
26
+ }
27
+
28
+ if (auth.authenticated) return true
29
+ if (auth.initializing || auth.error) return false
30
+
31
+ await auth.login(to.fullPath)
32
+ return false
33
+ }
34
+ }
35
+
@@ -0,0 +1,12 @@
1
+ export type AuthSnapshot = {
2
+ initialized: boolean
3
+ authenticated: boolean
4
+ token: string | null
5
+ subject: string | null
6
+ displayName: string | null
7
+ preferredUsername: string | null
8
+ email: string | null
9
+ realmRoles: string[]
10
+ resourceRoles: Record<string, string[]>
11
+ roles: string[]
12
+ }
@@ -0,0 +1,209 @@
1
+ import { computed, ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+ import {
4
+ getAuthSnapshot,
5
+ initializeAuth,
6
+ loginWithKeycloak,
7
+ logoutFromKeycloak,
8
+ subscribeAuth,
9
+ } from './keycloak'
10
+ import type { AuthSnapshot } from './types'
11
+ import { readAppEnv } from '../env/runtimeConfig'
12
+
13
+ const adminRole = readAppEnv('VITE_KEYCLOAK_ROLE_ADMIN')
14
+
15
+ function normalizeRoleKey(role: string): string {
16
+ return String(role ?? '')
17
+ .trim()
18
+ .toLowerCase()
19
+ .replace(/^\/+/, '')
20
+ .replace(/[\s._:]+/g, '-')
21
+ }
22
+
23
+ function toKeycloakInitErrorMessage(error: unknown): string {
24
+ if (error instanceof Error && error.message.trim().length > 0) return error.message.trim()
25
+ return 'Unable to initialize Keycloak. Check the UI env vars and the client redirect URI settings.'
26
+ }
27
+
28
+ function toFriendlyRoleLabel(role: string): string {
29
+ const value = String(role ?? '').trim()
30
+ const normalizedRole = normalizeRoleKey(value)
31
+ if (!value) return 'User'
32
+
33
+ switch (normalizedRole) {
34
+ case 'ngb-admin':
35
+ case 'admin':
36
+ case 'administrator':
37
+ case 'realm-admin':
38
+ return 'Administrator'
39
+ case 'ngb-user':
40
+ case 'user':
41
+ return 'User'
42
+ default: {
43
+ const normalized = value
44
+ .replace(/^ngb[-_:]?/i, '')
45
+ .replace(/[_-]+/g, ' ')
46
+ .trim()
47
+
48
+ const source = normalized || value
49
+ return source
50
+ .split(/\s+/g)
51
+ .filter(Boolean)
52
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
53
+ .join(' ')
54
+ }
55
+ }
56
+ }
57
+
58
+ function rolePriority(role: string): number {
59
+ switch (normalizeRoleKey(role)) {
60
+ case 'ngb-admin':
61
+ case 'admin':
62
+ case 'administrator':
63
+ case 'realm-admin':
64
+ return 0
65
+ case 'ngb-user':
66
+ case 'user':
67
+ return 10
68
+ default:
69
+ return 100
70
+ }
71
+ }
72
+
73
+ export const useAuthStore = defineStore('auth', () => {
74
+ const initialized = ref(false)
75
+ const initializing = ref(false)
76
+ const redirecting = ref(false)
77
+ const authenticated = ref(false)
78
+ const token = ref<string | null>(null)
79
+ const subject = ref<string | null>(null)
80
+ const displayName = ref<string | null>(null)
81
+ const preferredUsername = ref<string | null>(null)
82
+ const email = ref<string | null>(null)
83
+ const realmRoles = ref<string[]>([])
84
+ const resourceRoles = ref<Record<string, string[]>>({})
85
+ const roles = ref<string[]>([])
86
+ const error = ref<string | null>(null)
87
+
88
+ let initializePromise: Promise<void> | null = null
89
+
90
+ function applySnapshot(snapshot: AuthSnapshot): void {
91
+ initialized.value = snapshot.initialized
92
+ authenticated.value = snapshot.authenticated
93
+ token.value = snapshot.token
94
+ subject.value = snapshot.subject
95
+ displayName.value = snapshot.displayName
96
+ preferredUsername.value = snapshot.preferredUsername
97
+ email.value = snapshot.email
98
+ realmRoles.value = snapshot.realmRoles
99
+ resourceRoles.value = snapshot.resourceRoles
100
+ roles.value = snapshot.roles
101
+ }
102
+
103
+ subscribeAuth(applySnapshot)
104
+ applySnapshot(getAuthSnapshot())
105
+
106
+ const userName = computed(() => displayName.value || preferredUsername.value || email.value || 'User')
107
+ const isAdmin = computed(() => {
108
+ const normalizedConfiguredAdminRole = normalizeRoleKey(adminRole)
109
+ return roles.value.some((role) => {
110
+ const normalizedRole = normalizeRoleKey(role)
111
+ return normalizedRole === 'ngb-admin'
112
+ || normalizedRole === 'admin'
113
+ || normalizedRole === 'administrator'
114
+ || normalizedRole === 'realm-admin'
115
+ || (!!normalizedConfiguredAdminRole && normalizedRole === normalizedConfiguredAdminRole)
116
+ })
117
+ })
118
+ const primaryTechnicalRole = computed(() => {
119
+ const sorted = [...roles.value].sort((a, b) => rolePriority(a) - rolePriority(b) || a.localeCompare(b))
120
+ return sorted[0] ?? ''
121
+ })
122
+ const friendlyRoles = computed(() => {
123
+ const sorted = [...roles.value].sort((a, b) => rolePriority(a) - rolePriority(b) || a.localeCompare(b))
124
+ return Array.from(new Set(sorted.map((role) => toFriendlyRoleLabel(role))))
125
+ })
126
+ const primaryRoleLabel = computed(() => toFriendlyRoleLabel(primaryTechnicalRole.value))
127
+ const primaryRoleIcon = computed<'shield-check' | 'user'>(() =>
128
+ rolePriority(primaryTechnicalRole.value) === 0 ? 'shield-check' : 'user')
129
+
130
+ async function initialize(): Promise<void> {
131
+ if (initialized.value) return
132
+ if (initializePromise) return await initializePromise
133
+
134
+ initializing.value = true
135
+ error.value = null
136
+
137
+ initializePromise = initializeAuth()
138
+ .then((snapshot) => {
139
+ applySnapshot(snapshot)
140
+ })
141
+ .catch((cause) => {
142
+ error.value = toKeycloakInitErrorMessage(cause)
143
+ throw cause
144
+ })
145
+ .finally(() => {
146
+ initializing.value = false
147
+ initializePromise = null
148
+ })
149
+
150
+ return await initializePromise
151
+ }
152
+
153
+ async function login(targetPath?: string | null): Promise<void> {
154
+ redirecting.value = true
155
+ error.value = null
156
+
157
+ try {
158
+ await loginWithKeycloak(targetPath)
159
+ } catch (cause) {
160
+ redirecting.value = false
161
+ error.value = toKeycloakInitErrorMessage(cause)
162
+ throw cause
163
+ }
164
+ }
165
+
166
+ async function logout(): Promise<void> {
167
+ redirecting.value = true
168
+ error.value = null
169
+
170
+ try {
171
+ await logoutFromKeycloak()
172
+ } catch (cause) {
173
+ redirecting.value = false
174
+ error.value = toKeycloakInitErrorMessage(cause)
175
+ throw cause
176
+ }
177
+ }
178
+
179
+ function hasRole(role: string): boolean {
180
+ const value = String(role ?? '').trim()
181
+ return value.length > 0 && roles.value.includes(value)
182
+ }
183
+
184
+ return {
185
+ initialized,
186
+ initializing,
187
+ redirecting,
188
+ authenticated,
189
+ token,
190
+ subject,
191
+ displayName,
192
+ preferredUsername,
193
+ email,
194
+ realmRoles,
195
+ resourceRoles,
196
+ roles,
197
+ userName,
198
+ isAdmin,
199
+ primaryTechnicalRole,
200
+ friendlyRoles,
201
+ primaryRoleLabel,
202
+ primaryRoleIcon,
203
+ error,
204
+ initialize,
205
+ login,
206
+ logout,
207
+ hasRole,
208
+ }
209
+ })