@antelopejs/dms 0.8.3 → 0.9.1

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 (424) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/data/module-store/catalog.json +71 -0
  3. package/dist/config.d.ts +7 -0
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  7. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  8. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  9. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  10. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  11. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  12. package/dist/implementations/dms/frontend-modules.d.ts +6 -2
  13. package/dist/implementations/dms/frontend-modules.js +37 -8
  14. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  15. package/dist/implementations/dms/frontend-ready.d.ts +8 -0
  16. package/dist/implementations/dms/frontend-ready.js +34 -0
  17. package/dist/implementations/dms/frontend-ready.js.map +1 -0
  18. package/dist/implementations/dms/manifest.d.ts +7 -0
  19. package/dist/implementations/dms/manifest.js +13 -0
  20. package/dist/implementations/dms/manifest.js.map +1 -1
  21. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  22. package/dist/implementations/dms/menu-categories.js +30 -0
  23. package/dist/implementations/dms/menu-categories.js.map +1 -0
  24. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  25. package/dist/implementations/dms/nav-badges.js +0 -16
  26. package/dist/implementations/dms/nav-badges.js.map +1 -1
  27. package/dist/implementations/dms/page.d.ts +13 -5
  28. package/dist/implementations/dms/page.js +72 -41
  29. package/dist/implementations/dms/page.js.map +1 -1
  30. package/dist/implementations/dms-html-render/index.js +5 -2
  31. package/dist/implementations/dms-html-render/index.js.map +1 -1
  32. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  33. package/dist/implementations/dms-notifications/index.js +14 -6
  34. package/dist/implementations/dms-notifications/index.js.map +1 -1
  35. package/dist/pages/settings/settings-index.js +3 -2
  36. package/dist/pages/settings/settings-index.js.map +1 -1
  37. package/dist/pages/settings/users/invites.js +2 -2
  38. package/dist/pages/settings/users/invites.js.map +1 -1
  39. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  40. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  41. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  42. package/dist/pages/settings/users/member-invite-form.js +7 -4
  43. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  44. package/dist/pages/settings/users/member-roles-field.js +2 -1
  45. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  46. package/dist/pages/settings/users/members.js +4 -1
  47. package/dist/pages/settings/users/members.js.map +1 -1
  48. package/dist/pages/settings/users/notifications.d.ts +6 -0
  49. package/dist/pages/settings/users/notifications.js +23 -15
  50. package/dist/pages/settings/users/notifications.js.map +1 -1
  51. package/dist/pages/settings/users/profile.js +1 -1
  52. package/dist/pages/settings/users/profile.js.map +1 -1
  53. package/dist/pages/settings/users/region-form.d.ts +1 -1
  54. package/dist/pages/settings/users/region-form.js +2 -1
  55. package/dist/pages/settings/users/region-form.js.map +1 -1
  56. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  57. package/dist/pages/settings/users/role-editor-store.js +17 -4
  58. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  59. package/dist/pages/settings/users/roles.js +1 -0
  60. package/dist/pages/settings/users/roles.js.map +1 -1
  61. package/dist/realtime/registry.d.ts +1 -0
  62. package/dist/realtime/registry.js +5 -0
  63. package/dist/realtime/registry.js.map +1 -1
  64. package/dist/routes/index.d.ts +1 -0
  65. package/dist/routes/index.js +1 -0
  66. package/dist/routes/index.js.map +1 -1
  67. package/dist/routes/realtime.d.ts +5 -1
  68. package/dist/routes/realtime.js +16 -9
  69. package/dist/routes/realtime.js.map +1 -1
  70. package/dist/routes/store.d.ts +21 -0
  71. package/dist/routes/store.js +130 -0
  72. package/dist/routes/store.js.map +1 -0
  73. package/dist/store/activity.d.ts +2 -0
  74. package/dist/store/activity.js +12 -0
  75. package/dist/store/activity.js.map +1 -0
  76. package/dist/store/cache.d.ts +8 -0
  77. package/dist/store/cache.js +29 -0
  78. package/dist/store/cache.js.map +1 -0
  79. package/dist/store/catalog.d.ts +4 -0
  80. package/dist/store/catalog.js +69 -0
  81. package/dist/store/catalog.js.map +1 -0
  82. package/dist/store/compatibility.d.ts +3 -0
  83. package/dist/store/compatibility.js +45 -0
  84. package/dist/store/compatibility.js.map +1 -0
  85. package/dist/store/declarations.d.ts +18 -0
  86. package/dist/store/declarations.js +116 -0
  87. package/dist/store/declarations.js.map +1 -0
  88. package/dist/store/frontend-compatibility.d.ts +18 -0
  89. package/dist/store/frontend-compatibility.js +93 -0
  90. package/dist/store/frontend-compatibility.js.map +1 -0
  91. package/dist/store/interfaces.d.ts +6 -0
  92. package/dist/store/interfaces.js +126 -0
  93. package/dist/store/interfaces.js.map +1 -0
  94. package/dist/store/listing.d.ts +2 -0
  95. package/dist/store/listing.js +180 -0
  96. package/dist/store/listing.js.map +1 -0
  97. package/dist/store/operations.d.ts +8 -0
  98. package/dist/store/operations.js +254 -0
  99. package/dist/store/operations.js.map +1 -0
  100. package/dist/store/registry.d.ts +4 -0
  101. package/dist/store/registry.js +61 -0
  102. package/dist/store/registry.js.map +1 -0
  103. package/dist/store/requirements.d.ts +14 -0
  104. package/dist/store/requirements.js +81 -0
  105. package/dist/store/requirements.js.map +1 -0
  106. package/dist/store/runtime.d.ts +11 -0
  107. package/dist/store/runtime.js +78 -0
  108. package/dist/store/runtime.js.map +1 -0
  109. package/dist/store/tarball.d.ts +2 -0
  110. package/dist/store/tarball.js +80 -0
  111. package/dist/store/tarball.js.map +1 -0
  112. package/dist/store/types.d.ts +98 -0
  113. package/dist/store/types.js +3 -0
  114. package/dist/store/types.js.map +1 -0
  115. package/dist/test/integration/account-deletion.test.js +2 -2
  116. package/dist/test/integration/account-deletion.test.js.map +1 -1
  117. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  118. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  119. package/dist/test/integration/auth/current-password.test.js +2 -1
  120. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  121. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  122. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  123. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  124. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  125. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  126. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  127. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  128. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  129. package/dist/test/integration/email-change.test.js +2 -1
  130. package/dist/test/integration/email-change.test.js.map +1 -1
  131. package/dist/test/integration/files/component-files.test.js +2 -2
  132. package/dist/test/integration/files/component-files.test.js.map +1 -1
  133. package/dist/test/integration/member-settings-access.test.js +8 -7
  134. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  135. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  136. package/dist/test/integration/menu-access-events.test.js +148 -0
  137. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  138. package/dist/test/integration/notification-inbox.test.js +4 -1
  139. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  140. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  141. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  142. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  143. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  144. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  145. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  146. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  147. package/dist/test/integration/permission-warnings.test.js +3 -2
  148. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  149. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  150. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  151. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  152. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  153. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  154. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  155. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  156. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  157. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  158. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  159. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  160. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  161. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  162. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  163. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  164. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  165. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  166. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  167. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  168. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  169. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  170. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  171. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  172. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  173. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  174. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  175. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  176. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  177. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  178. package/dist/test/unit/store/store.test.d.ts +1 -0
  179. package/dist/test/unit/store/store.test.js +127 -0
  180. package/dist/test/unit/store/store.test.js.map +1 -0
  181. package/dist/validation/store.schema.d.ts +39 -0
  182. package/dist/validation/store.schema.js +27 -0
  183. package/dist/validation/store.schema.js.map +1 -0
  184. package/frontend-vue/deferred-plugins.ts +3 -4
  185. package/frontend-vue/dms.email.ts +5 -1
  186. package/frontend-vue/dms.frontend.ts +15 -5
  187. package/frontend-vue/dms.server.ts +74 -0
  188. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  189. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  190. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  191. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  192. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  193. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  194. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  195. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  196. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  197. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  198. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  199. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  200. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  201. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  202. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  203. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  204. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  205. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  206. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  207. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  208. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  209. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  210. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  211. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  212. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  213. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  214. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  215. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  216. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  217. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  218. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  219. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  220. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  221. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  222. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  223. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  224. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  225. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  226. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  227. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  228. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  229. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  230. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  231. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
  232. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
  233. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
  234. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
  235. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  236. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  237. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  238. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  239. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  240. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  241. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  242. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  243. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  244. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  245. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  246. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  247. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  248. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  249. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  250. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  251. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  252. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  253. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  254. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  255. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  256. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
  257. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
  258. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  259. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  260. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  261. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  262. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  263. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  264. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
  265. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  266. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  267. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  268. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  269. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +341 -62
  270. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  271. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  272. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  273. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  274. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  275. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  276. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  277. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  278. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  279. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  280. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  281. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
  282. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  283. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
  284. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
  285. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  286. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  287. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  288. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  289. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  291. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  293. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  294. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  295. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  296. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  301. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  302. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  303. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  304. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  305. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  306. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  307. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  308. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  309. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  310. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  311. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  312. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  313. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  314. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  315. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  316. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  317. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  318. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  319. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  320. package/frontend-vue/package.json +3 -3
  321. package/frontend-vue/pnpm-lock.yaml +16 -22
  322. package/frontend-vue/server/private-options.ts +27 -0
  323. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  324. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  325. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  326. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  327. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  328. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  329. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  330. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  331. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  332. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  333. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  334. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  335. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  336. package/frontend-vue/tests/email-layout.test.ts +66 -4
  337. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  338. package/frontend-vue/tests/favicon.test.ts +131 -0
  339. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  340. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  341. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  342. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  343. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  344. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  345. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  346. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  347. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  348. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  349. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  350. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  351. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  352. package/frontend-vue/tests/module-routing.test.ts +2 -5
  353. package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
  354. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  355. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  356. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  357. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  358. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  359. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  360. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  361. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  362. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  363. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  364. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  365. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  366. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  367. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  368. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  369. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  370. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  371. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  372. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  373. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  374. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  375. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  376. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  377. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  378. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  379. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  380. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  381. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  382. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  383. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  384. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  385. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  386. package/frontend-vue/tests/server-routes.test.ts +582 -0
  387. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  388. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  389. package/frontend-vue/tests/side-panels.test.ts +8 -1
  390. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  391. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  392. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  393. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  394. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  395. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  396. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  397. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  398. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  399. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  400. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  401. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  402. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  403. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  404. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  405. package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
  406. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  407. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  408. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  409. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  410. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  411. package/frontend-vue/tests/user-language.test.ts +8 -5
  412. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  413. package/frontend-vue/vitest.config.ts +6 -0
  414. package/package.json +2 -2
  415. package/skills/dms/SKILL.md +2 -1
  416. package/skills/dms-module/SKILL.md +1 -1
  417. package/skills/dms-pages/SKILL.md +4 -1
  418. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  419. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  420. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  421. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  422. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  423. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  424. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -1,7 +1,11 @@
1
1
  import type { Component } from "vue";
2
2
  import defaults from "./layers/dms-layout/app/app.config";
3
3
 
4
- /** Branding shared with transactional templates, without the browser module. */
4
+ /**
5
+ * Branding shared with transactional templates, without the browser module.
6
+ * The loader merges the layout's app config into theirs as well; `branding`
7
+ * holds no array that the second merge would repeat.
8
+ */
5
9
  export const appConfig = { branding: defaults.branding };
6
10
 
7
11
  interface EmailModule {
@@ -1,17 +1,17 @@
1
1
  import { defineAsyncComponent, type Component } from "vue";
2
2
  import {
3
3
  type DmsFrontendModule,
4
- useDmsAppConfig,
5
4
  useDmsRuntimeConfig,
6
5
  } from "#dms/frontend-module";
7
- import { defu } from "defu";
8
- import appConfig from "./layers/dms-layout/app/app.config";
9
6
  import authMiddleware from "./layers/dms-auth/app/middleware/auth";
7
+ import permissionsPreloadPlugin from "./layers/dms-auth/app/plugins/permissions-preload";
8
+ import asyncDataScopePlugin from "./layers/dms-core/app/plugins/async-data-scope";
10
9
  import homepageRedirectMiddleware from "./layers/dms-layout/app/middleware/homepage-redirect.global";
11
10
  import moduleRoutingMiddleware from "./layers/dms-layout/app/middleware/module-routing.global";
12
11
  import pageLeaveGuardMiddleware from "./layers/dms-layout/app/middleware/page-leave-guard.global";
13
12
  import accessibilityPlugin from "./layers/dms-layout/app/plugins/accessibility";
14
13
  import colorModePlugin from "./layers/dms-layout/app/plugins/color-mode";
14
+ import faviconPlugin from "./layers/dms-layout/app/plugins/favicon";
15
15
  import interfaceScalePlugin from "./layers/dms-layout/app/plugins/interface-scale";
16
16
  import fontPreloadPlugin from "./layers/dms-layout/app/plugins/font-preload";
17
17
  import permissionPreviewPrepaintPlugin from "./layers/dms-layout/app/plugins/permission-preview-prepaint";
@@ -19,6 +19,7 @@ import regionalPreferencesPlugin from "./layers/dms-layout/app/plugins/regional-
19
19
  import seoPlugin from "./layers/dms-layout/app/plugins/seo";
20
20
  import sidePanelHostPlugin from "./layers/dms-layout/app/plugins/side-panel-host";
21
21
  import onboardingMiddleware from "./layers/dms-onboarding/app/middleware/onboarding.global";
22
+ import onboardingPreloadPlugin from "./layers/dms-onboarding/app/plugins/onboarding-preload";
22
23
  import registerPlugin from "./layers/dms-ui/app/plugins/register";
23
24
  import shortcutsPlugin from "./layers/dms-ui/app/plugins/shortcuts";
24
25
  import tableViewDisplaysPlugin from "./layers/dms-ui/app/plugins/table-view-displays";
@@ -164,6 +165,13 @@ function registerPlugins(sdk: Parameters<DmsFrontendModule["setup"]>[0]): void {
164
165
  await plugin.default(context);
165
166
  }, clientOnly);
166
167
  }
168
+ // Before any data is read: every key lives in the session's scope.
169
+ sdk.registerPlugin(asyncDataScopePlugin);
170
+ // First: the requests the middlewares wait on start with the app, alongside
171
+ // the ones the plugins below and the page make, instead of one after the
172
+ // other once the page has set up.
173
+ sdk.registerPlugin(onboardingPreloadPlugin);
174
+ sdk.registerPlugin(permissionsPreloadPlugin);
167
175
  sdk.registerPlugin(colorModePlugin);
168
176
  sdk.registerPlugin(permissionPreviewPrepaintPlugin);
169
177
  sdk.registerPlugin(fontPreloadPlugin);
@@ -171,6 +179,7 @@ function registerPlugins(sdk: Parameters<DmsFrontendModule["setup"]>[0]): void {
171
179
  sdk.registerPlugin(accessibilityPlugin);
172
180
  sdk.registerPlugin(regionalPreferencesPlugin);
173
181
  sdk.registerPlugin(seoPlugin);
182
+ sdk.registerPlugin(faviconPlugin);
174
183
  sdk.registerPlugin(sidePanelHostPlugin);
175
184
  sdk.registerPlugin(registerPlugin);
176
185
  sdk.registerPlugin(shortcutsPlugin);
@@ -195,12 +204,13 @@ function registerMiddleware(
195
204
 
196
205
  const frontendModule: DmsFrontendModule = {
197
206
  componentPrefix: "Dms",
207
+ // The layout's app config is not merged here: the loader applies every
208
+ // layer's `app/app.config.ts` before any module is set up, and a second
209
+ // merge would repeat the arrays defu concatenates.
198
210
  setup(sdk) {
199
211
  const runtimeDms = (useDmsRuntimeConfig().public as DmsPublicOptions).dms;
200
212
  if (runtimeDms && typeof window !== "undefined")
201
213
  runtimeDms.baseURL = window.location.origin;
202
- const runtimeAppConfig = useDmsAppConfig();
203
- Object.assign(runtimeAppConfig, defu(runtimeAppConfig, appConfig));
204
214
  registerPlugins(sdk);
205
215
  registerMiddleware(sdk);
206
216
  registerComponents(sdk);
@@ -0,0 +1,74 @@
1
+ import { defineDmsServer } from "#dms/server";
2
+ import * as actions from "./layers/dms-auth/server/actions";
3
+ import * as oauth from "./layers/dms-auth/server/oauth";
4
+ import * as session from "./layers/dms-auth/server/session";
5
+ import { renderHtml } from "./layers/dms-layout/server/html-render";
6
+
7
+ // What the DMS serves from the frontend server: its session, sign-in and
8
+ // provider sign-in routes, the session logic the server runs outside them,
9
+ // the e-mail render the backend asks for, and the backend prefixes the
10
+ // browser reaches through it. The renderer proxies no path on its own.
11
+ export default defineDmsServer((server) => {
12
+ // The backend's API and the DMS's own routes (menu, page, page layouts,
13
+ // permissions, realtime), with the session's token added server-side.
14
+ server.registerBackendProxy("/api/**");
15
+ server.registerBackendProxy("/dms/**");
16
+
17
+ server.registerRoute("GET", "/api/_auth/session", session.readSessionRoute);
18
+ server.registerRoute(
19
+ "POST",
20
+ "/api/_auth/session",
21
+ session.refreshSessionRoute,
22
+ );
23
+ server.registerRoute("DELETE", "/api/_auth/session", session.logoutRoute);
24
+ server.registerRoute(
25
+ ["HEAD", "PUT", "PATCH", "OPTIONS"],
26
+ "/api/_auth/session",
27
+ session.sessionMethodNotAllowed,
28
+ { sameOrigin: false },
29
+ );
30
+
31
+ server.registerRoute("POST", "/auth/login", actions.login);
32
+ server.registerRoute("POST", "/auth/register", actions.signup);
33
+ server.registerRoute("POST", "/auth/verify-2fa", actions.verifyTwoFactor);
34
+ server.registerRoute(
35
+ "POST",
36
+ "/auth/request-2fa-email",
37
+ actions.requestTwoFactorEmail,
38
+ );
39
+ server.registerRoute("POST", "/auth/establish", actions.establish);
40
+ server.registerRoute("POST", "/auth/switch-account", actions.switchAccount);
41
+ server.registerRoute("POST", "/auth/switch-tenant", actions.switchTenant);
42
+ server.registerRoute(
43
+ "POST",
44
+ "/auth/validate-account",
45
+ actions.validateAccount,
46
+ );
47
+ server.registerRoute(
48
+ "POST",
49
+ "/auth/remove-account",
50
+ actions.removeStoredAccount,
51
+ );
52
+
53
+ server.registerRoute("GET", "/auth/oauth/:provider/start", oauth.oauthStart);
54
+ server.registerRoute(
55
+ "GET",
56
+ "/auth/oauth/:provider/callback",
57
+ oauth.oauthCallback,
58
+ );
59
+ server.registerRoute("POST", "/auth/oauth/handoff", oauth.oauthHandoff);
60
+
61
+ server.registerAuth({
62
+ signInPath: "/auth",
63
+ signInExempt: ["/auth/**", "/onboarding"],
64
+ refreshSession: session.renewSession,
65
+ publicSession: session.publicSession,
66
+ });
67
+
68
+ // The backend authenticates with a single-use service token, not a
69
+ // browser session, and its default `renderEndpoint` names this path.
70
+ server.registerRoute("POST", "/api/html/render", renderHtml, {
71
+ sameOrigin: false,
72
+ overridesBackend: true,
73
+ });
74
+ });
@@ -13,7 +13,7 @@ const FALLBACK_AUTH_ROUTE = "/auth";
13
13
  * Any "Preview as role" state goes too.
14
14
  */
15
15
  export function useLogout() {
16
- const { user, clear: clearSession } = useUserSession<User>();
16
+ const { user, clear: clearSession } = useDmsUserSession<User>();
17
17
  const { accounts, removeAccount } = useMultiAccount();
18
18
  const { purge: purgePermissionPreviews } = usePermissionPreview();
19
19
 
@@ -1,9 +1,22 @@
1
1
  type PostLoginDestination = string | (() => string | undefined);
2
2
 
3
+ interface PostLoginRedirectOptions {
4
+ /**
5
+ * Open the destination in place of the current page in the history. For a
6
+ * page that consumed a one-time sign-in token (the OAuth completion, the
7
+ * second factor): back on it, the user would only meet a dead token, or a
8
+ * page sending them forward again.
9
+ */
10
+ replace?: boolean;
11
+ }
12
+
3
13
  /** Refresh authenticated state before resolving and opening the destination. */
4
- export async function usePostLoginRedirect(redirectUrl?: PostLoginDestination) {
14
+ export async function usePostLoginRedirect(
15
+ redirectUrl?: PostLoginDestination,
16
+ options: PostLoginRedirectOptions = {},
17
+ ) {
5
18
  const dmsApp = useDmsApp();
6
- const { fetch: refreshUser, user } = useUserSession();
19
+ const { fetch: refreshUser, user } = useDmsUserSession();
7
20
  const { addCurrentAccount } = useMultiAccount();
8
21
  const siteLayout = useSiteLayout();
9
22
  const homepage = useHomepage();
@@ -24,5 +37,5 @@ export async function usePostLoginRedirect(redirectUrl?: PostLoginDestination) {
24
37
 
25
38
  const destination =
26
39
  typeof redirectUrl === "function" ? redirectUrl() : redirectUrl;
27
- await navigateDms(destination || homepage);
40
+ await navigateDms(destination || homepage, options);
28
41
  }
@@ -115,7 +115,9 @@ async function verify() {
115
115
  },
116
116
  });
117
117
 
118
- await usePostLoginRedirect();
118
+ // The sign-in token in this page's URL is spent: the destination takes
119
+ // its place in the history.
120
+ await usePostLoginRedirect(undefined, { replace: true });
119
121
  } catch (error: unknown) {
120
122
  pin.value = [];
121
123
  await showError(error, "page.2fa.error_title", {
@@ -63,7 +63,9 @@ async function onSubmit(payload: FormSubmitEvent<Schema>) {
63
63
  },
64
64
  });
65
65
 
66
- await usePostLoginRedirect();
66
+ // The sign-in token in this page's URL is spent: the destination takes
67
+ // its place in the history.
68
+ await usePostLoginRedirect(undefined, { replace: true });
67
69
  } catch (error: unknown) {
68
70
  // A wrong code shows under the field; an expired sign-in above the form.
69
71
  await showError(error, "page.backup.error_title", {
@@ -19,7 +19,7 @@ const { formError, showFormError, clearFormError, showError } =
19
19
  useAuthFormError();
20
20
 
21
21
  if (!route.query.email) {
22
- throw createError({
22
+ throw createDmsError({
23
23
  statusCode: HTTP_BAD_REQUEST,
24
24
  statusMessage: INVALID_REQUEST,
25
25
  message: "Missing required email parameter",
@@ -2,6 +2,7 @@
2
2
  import { type OAuthHandoff, sanitizeRedirectPath } from "../../../shared/oauth";
3
3
 
4
4
  const OAUTH_FAILED_KEY = "error.oauth.failed";
5
+ const IN_FLIGHT_COMPLETION_KEY = "__dmsInFlightOAuthCompletion";
5
6
 
6
7
  const route = useDmsRoute();
7
8
  const homepage = useHomepage();
@@ -39,7 +40,7 @@ const HANDOFF_ACTIONS: HandoffActions = {
39
40
  },
40
41
  REPLACE,
41
42
  ),
42
- none: () => usePostLoginRedirect(redirect.value),
43
+ none: () => usePostLoginRedirect(redirect.value, REPLACE),
43
44
  };
44
45
 
45
46
  function runHandoffAction(handoff: OAuthHandoff): unknown {
@@ -56,7 +57,7 @@ function redirectToLoginWithError(): unknown {
56
57
  );
57
58
  }
58
59
 
59
- onMounted(async () => {
60
+ async function completeOAuth(): Promise<void> {
60
61
  try {
61
62
  const handoff = await $fetch<OAuthHandoff>("/auth/oauth/handoff", {
62
63
  method: "POST",
@@ -65,7 +66,29 @@ onMounted(async () => {
65
66
  } catch {
66
67
  await dmsApp.runWithContext(() => redirectToLoginWithError());
67
68
  }
68
- });
69
+ }
70
+
71
+ // One completion per visit, held on the DMS app rather than by this instance.
72
+ // The post-login redirect refreshes the site layout for the new identity,
73
+ // which empties the page layout cache: the page unmounts this component and
74
+ // mounts a new one while its layout is fetched again. Starting over from that
75
+ // mount consumed the handoff once more and refreshed again, so the page
76
+ // remounted in a loop instead of leaving. The new instance joins the running
77
+ // completion; a later visit, once it has settled, starts its own.
78
+ function joinCompletion(): Promise<void> {
79
+ const holder = dmsApp as unknown as Record<PropertyKey, unknown>;
80
+ const running = holder[IN_FLIGHT_COMPLETION_KEY] as Promise<void> | undefined;
81
+ if (running) return running;
82
+ const completion = completeOAuth().finally(() => {
83
+ if (holder[IN_FLIGHT_COMPLETION_KEY] === completion) {
84
+ holder[IN_FLIGHT_COMPLETION_KEY] = undefined;
85
+ }
86
+ });
87
+ holder[IN_FLIGHT_COMPLETION_KEY] = completion;
88
+ return completion;
89
+ }
90
+
91
+ onMounted(joinCompletion);
69
92
  </script>
70
93
 
71
94
  <template>
@@ -23,7 +23,7 @@ const { formError, clearFormError, showError } = useAuthFormError();
23
23
  const form = useTemplateRef<AuthFormHandle>("form");
24
24
 
25
25
  if (!route.query.token || !route.query.email) {
26
- throw createError({
26
+ throw createDmsError({
27
27
  statusCode: HTTP_BAD_REQUEST,
28
28
  statusMessage: INVALID_REQUEST,
29
29
  message: "Missing required authentication parameters",
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { onMounted, useTemplateRef } from "vue";
2
+ import { useTemplateRef } from "vue";
3
3
  import * as z from "zod";
4
4
  import type { FormSubmitEvent } from "@nuxt/ui";
5
5
  import StageCard from "../../../../dms-layout/app/build/components/layout/StageCard.vue";
@@ -78,7 +78,6 @@ const config = useDmsRuntimeConfig();
78
78
  const homepage = useHomepage();
79
79
  const dmsApp = useDmsApp();
80
80
  const { formError, clearFormError, showError } = useAuthFormError();
81
- const { $authFetch } = useAuthFetch();
82
81
  const form = useTemplateRef<AuthFormHandle>("form");
83
82
 
84
83
  const route = useDmsRoute();
@@ -91,34 +90,32 @@ const hasInvitationToken = computed(
91
90
  () => typeof queryToken.value === "string" && queryToken.value !== "",
92
91
  );
93
92
 
94
- const invitationState = ref<InvitationState>(
95
- hasInvitationToken.value ? "checking" : "invalid",
93
+ // A revoked, replaced or expired link says so as the page opens, rather than
94
+ // after the whole form was filled: checked with the page, so the server
95
+ // renders the notice. The server answers with the very check the signup
96
+ // runs; any other failure leaves the form to the signup to settle.
97
+ const invitationCheck = useApiData(
98
+ () => (hasInvitationToken.value ? "/api/auth/validate-invite-token" : null),
99
+ {
100
+ method: "POST",
101
+ body: () => ({ token: queryToken.value, email: queryEmail.value }),
102
+ },
96
103
  );
104
+ // The invitation can be retired while the form is open: the signup says so.
105
+ const closedOnSignup = ref<ClosedInvitation>();
106
+ const invitationState = computed<InvitationState>(() => {
107
+ if (!hasInvitationToken.value) return "invalid";
108
+ if (closedOnSignup.value) return closedOnSignup.value;
109
+ const { status, error } = invitationCheck;
110
+ if (status.value === "idle" || status.value === "pending") return "checking";
111
+ return error.value ? (closedInvitationOf(error.value) ?? "open") : "open";
112
+ });
97
113
  const closedInvitation = computed(() =>
98
114
  invitationState.value in CLOSED_INVITATION_NOTICES
99
115
  ? CLOSED_INVITATION_NOTICES[invitationState.value as ClosedInvitation]
100
116
  : undefined,
101
117
  );
102
118
 
103
- // A revoked, replaced or expired link says so as the page opens, rather than
104
- // after the whole form was filled. The server answers with the very check the
105
- // signup runs; any other failure leaves the form to the signup to settle.
106
- async function checkInvitation(): Promise<void> {
107
- try {
108
- await $authFetch("/api/auth/validate-invite-token", {
109
- method: "POST",
110
- body: { token: queryToken.value, email: queryEmail.value },
111
- });
112
- invitationState.value = "open";
113
- } catch (error: unknown) {
114
- invitationState.value = closedInvitationOf(error) ?? "open";
115
- }
116
- }
117
-
118
- onMounted(() => {
119
- if (invitationState.value === "checking") void checkInvitation();
120
- });
121
-
122
119
  // The invitation link carries the language the invitee was invited in: the
123
120
  // page opens in it, and the footer switcher still lets them pick another one
124
121
  // before the account is created with whichever is current.
@@ -149,7 +146,7 @@ async function onSubmit(payload: FormSubmitEvent<Schema>) {
149
146
  isLoading.value = true;
150
147
  clearFormError();
151
148
 
152
- await $fetch("/auth/signup", {
149
+ await $fetch("/auth/register", {
153
150
  method: "POST",
154
151
  body: {
155
152
  ...payload.data,
@@ -167,7 +164,7 @@ async function onSubmit(payload: FormSubmitEvent<Schema>) {
167
164
  // The invitation can be retired while the form is open.
168
165
  const closed = closedInvitationOf(error);
169
166
  if (closed) {
170
- invitationState.value = closed;
167
+ closedOnSignup.value = closed;
171
168
  return;
172
169
  }
173
170
  // A refused value (an address already used) shows under its field;
@@ -19,7 +19,7 @@ const { formError, showFormError, clearFormError, showError } =
19
19
  useAuthFormError();
20
20
 
21
21
  if (!route.query.id) {
22
- throw createError({
22
+ throw createDmsError({
23
23
  statusCode: HTTP_BAD_REQUEST,
24
24
  statusMessage: INVALID_REQUEST,
25
25
  message: "Missing required id parameter",
@@ -10,12 +10,10 @@ function isAuthOptions(value: MiddlewareMeta): value is AuthMiddlewareOptions {
10
10
  }
11
11
 
12
12
  async function loadPermissionsIfNeeded() {
13
- const { isLoaded, fetchPermissions } = usePermissions();
14
-
15
- if (isLoaded.value) return;
13
+ const { loadPermissions } = usePermissions();
16
14
 
17
15
  try {
18
- await fetchPermissions();
16
+ await loadPermissions();
19
17
  } catch {
20
18
  /* ignore — permissions load failure is not fatal here */
21
19
  }
@@ -26,7 +24,7 @@ function handleAuthenticatedUser(
26
24
  options: MiddlewareMeta,
27
25
  ) {
28
26
  const authConfig = useDmsRuntimeConfig().public.dms;
29
- const { user } = useUserSession();
27
+ const { user } = useDmsUserSession();
30
28
 
31
29
  if (authConfig.mustValidateEmail && !user.value?.isValidated) {
32
30
  if (to.path === "/auth/validate") return;
@@ -76,14 +74,14 @@ export default defineDmsMiddleware(async (to) => {
76
74
  // is committed and rendered with its own status. Re-throwing it here would
77
75
  // reject the pending navigation the SSR render awaits, turning that 404 into
78
76
  // a 500 fallback render.
79
- if (useError().value) return abortNavigation();
77
+ if (useDmsError().value) return abortDmsNavigation();
80
78
 
81
79
  const options = to.meta.auth as MiddlewareMeta | undefined;
82
80
  if (!options) return;
83
81
 
84
82
  if (await isPublicAccessRoute(to)) return;
85
83
 
86
- const { loggedIn } = useUserSession();
84
+ const { loggedIn } = useDmsUserSession();
87
85
 
88
86
  if (loggedIn.value) {
89
87
  await loadPermissionsIfNeeded();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Loads the permission set as the app starts when the `auth` middleware will
3
+ * need it, rather than once the middlewares ahead of it are done: the request
4
+ * runs alongside the others the render makes, and the middleware joins it.
5
+ *
6
+ * Only then: a signed-in user opening a registered page that is not public,
7
+ * which is when the middleware loads the set. Anywhere else nothing is asked
8
+ * that was not asked before. A failure stays the middleware's to handle.
9
+ */
10
+ export default defineDmsPlugin(() => {
11
+ if (!useDmsUserSession().loggedIn.value) return;
12
+ const page = useSiteLayout().findMatchingRoute(useDmsRoute().path);
13
+ if (!page || page.metadata.publicAccess === true) return;
14
+ usePermissions()
15
+ .loadPermissions()
16
+ .catch(() => undefined);
17
+ });
@@ -0,0 +1,135 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { DmsServerEvent, DmsSession } from "#dms/server";
3
+
4
+ /**
5
+ * The accounts this browser is signed in to: an index of their descriptors,
6
+ * and one sealed cookie per account holding its refresh token. The frontend
7
+ * server scopes and seals the cookies; their names and contents are the DMS's,
8
+ * unchanged from when the renderer wrote them, so stored accounts survive the
9
+ * move.
10
+ */
11
+
12
+ const ACCOUNT_INDEX_COOKIE = "dms_account_index";
13
+ const ACCOUNT_COOKIE_PREFIX = "dms_account_";
14
+ const MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
15
+ const UUID =
16
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
17
+
18
+ export interface AccountDescriptor {
19
+ accountId: string;
20
+ userId?: string;
21
+ email?: string;
22
+ name?: string;
23
+ activeTenantId?: string;
24
+ persistent: boolean;
25
+ }
26
+
27
+ export interface StoredAccount extends AccountDescriptor {
28
+ refreshToken?: string;
29
+ }
30
+
31
+ type AccountIndex = Record<string, AccountDescriptor>;
32
+
33
+ interface SessionUser {
34
+ _id?: string;
35
+ id?: string;
36
+ email?: string;
37
+ name?: string;
38
+ }
39
+
40
+ /**
41
+ * Whether a session, or a stored account, outlives the browser. Only a login
42
+ * that left "keep me signed in" unchecked says no; a cookie written before
43
+ * the flag existed carries none, and keeps the lifetime it was given then.
44
+ */
45
+ export function isPersistent(session?: { persistent?: unknown }): boolean {
46
+ return session?.persistent !== false;
47
+ }
48
+
49
+ function lifetime(persistent: boolean): number | undefined {
50
+ return persistent ? MAX_AGE_SECONDS : undefined;
51
+ }
52
+
53
+ function accountCookieName(accountId: string): string {
54
+ if (!UUID.test(accountId)) throw new Error("Invalid account id");
55
+ return `${ACCOUNT_COOKIE_PREFIX}${accountId.replaceAll("-", "")}`;
56
+ }
57
+
58
+ function descriptor(session: DmsSession, accountId: string): AccountDescriptor {
59
+ const user = (session.user ?? {}) as SessionUser;
60
+ return {
61
+ accountId,
62
+ userId: user._id ?? user.id,
63
+ email: user.email,
64
+ name: user.name,
65
+ activeTenantId: session.activeTenantId,
66
+ persistent: isPersistent(session),
67
+ };
68
+ }
69
+
70
+ export function readAccounts(event: DmsServerEvent): AccountIndex {
71
+ return event.cookies.readSealed<AccountIndex>(ACCOUNT_INDEX_COOKIE) ?? {};
72
+ }
73
+
74
+ function writeAccountIndex(event: DmsServerEvent, accounts: AccountIndex) {
75
+ const persistent = Object.values(accounts).some(isPersistent);
76
+ event.cookies.writeSealed(ACCOUNT_INDEX_COOKIE, accounts, {
77
+ maxAge: lifetime(persistent),
78
+ });
79
+ }
80
+
81
+ export function readAccount(
82
+ event: DmsServerEvent,
83
+ accountId: unknown,
84
+ ): StoredAccount | undefined {
85
+ if (typeof accountId !== "string" || !UUID.test(accountId)) return undefined;
86
+ return event.cookies.readSealed<StoredAccount>(accountCookieName(accountId));
87
+ }
88
+
89
+ export function storeAccount(
90
+ event: DmsServerEvent,
91
+ session: DmsSession,
92
+ accountId?: string,
93
+ ): AccountDescriptor {
94
+ const id = accountId ?? randomUUID();
95
+ const account = descriptor(session, id);
96
+ writeAccountIndex(event, { ...readAccounts(event), [id]: account });
97
+ event.cookies.writeSealed(
98
+ accountCookieName(id),
99
+ { refreshToken: session.refreshToken, ...account },
100
+ { maxAge: lifetime(account.persistent) },
101
+ );
102
+ return account;
103
+ }
104
+
105
+ function assertAccountInvariant(session: DmsSession, accountId: string) {
106
+ if (!accountId || session.accountId !== accountId)
107
+ throw new Error("Account session invariant violated");
108
+ }
109
+
110
+ /**
111
+ * Write `session` as the current one, and as one of the stored accounts. The
112
+ * frontend server stamps it with `updatedAt`, which lets the browser tell a
113
+ * page fetched before this write, and restored from its cache, from a
114
+ * current one.
115
+ */
116
+ export function persistAccountSession(
117
+ event: DmsServerEvent,
118
+ session: DmsSession,
119
+ accountId?: string,
120
+ ): AccountDescriptor {
121
+ const id = accountId ?? session.accountId ?? randomUUID();
122
+ session.accountId = id;
123
+ const account = storeAccount(event, session, id);
124
+ assertAccountInvariant(session, account.accountId);
125
+ event.session.write(session);
126
+ return account;
127
+ }
128
+
129
+ export function removeAccount(event: DmsServerEvent, accountId: unknown) {
130
+ const accounts = { ...readAccounts(event) };
131
+ if (typeof accountId === "string") delete accounts[accountId];
132
+ writeAccountIndex(event, accounts);
133
+ if (typeof accountId === "string" && UUID.test(accountId))
134
+ event.cookies.clear(accountCookieName(accountId));
135
+ }