@antelopejs/dms 0.8.3 → 0.9.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 (351) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  3. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  4. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  5. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  6. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  7. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  8. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  9. package/dist/implementations/dms/frontend-modules.js +8 -7
  10. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  11. package/dist/implementations/dms/manifest.d.ts +6 -0
  12. package/dist/implementations/dms/manifest.js +13 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  15. package/dist/implementations/dms/menu-categories.js +30 -0
  16. package/dist/implementations/dms/menu-categories.js.map +1 -0
  17. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  18. package/dist/implementations/dms/nav-badges.js +0 -16
  19. package/dist/implementations/dms/nav-badges.js.map +1 -1
  20. package/dist/implementations/dms/page.d.ts +11 -5
  21. package/dist/implementations/dms/page.js +56 -41
  22. package/dist/implementations/dms/page.js.map +1 -1
  23. package/dist/implementations/dms-html-render/index.js +5 -2
  24. package/dist/implementations/dms-html-render/index.js.map +1 -1
  25. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  26. package/dist/implementations/dms-notifications/index.js +14 -6
  27. package/dist/implementations/dms-notifications/index.js.map +1 -1
  28. package/dist/pages/settings/settings-index.js +3 -2
  29. package/dist/pages/settings/settings-index.js.map +1 -1
  30. package/dist/pages/settings/users/invites.js +2 -2
  31. package/dist/pages/settings/users/invites.js.map +1 -1
  32. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  33. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  34. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  35. package/dist/pages/settings/users/member-invite-form.js +7 -4
  36. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  37. package/dist/pages/settings/users/member-roles-field.js +2 -1
  38. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  39. package/dist/pages/settings/users/members.js +4 -1
  40. package/dist/pages/settings/users/members.js.map +1 -1
  41. package/dist/pages/settings/users/notifications.d.ts +6 -0
  42. package/dist/pages/settings/users/notifications.js +23 -15
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/profile.js +1 -1
  45. package/dist/pages/settings/users/profile.js.map +1 -1
  46. package/dist/pages/settings/users/region-form.d.ts +1 -1
  47. package/dist/pages/settings/users/region-form.js +2 -1
  48. package/dist/pages/settings/users/region-form.js.map +1 -1
  49. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  50. package/dist/pages/settings/users/role-editor-store.js +17 -4
  51. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  52. package/dist/pages/settings/users/roles.js +1 -0
  53. package/dist/pages/settings/users/roles.js.map +1 -1
  54. package/dist/realtime/registry.d.ts +1 -0
  55. package/dist/realtime/registry.js +5 -0
  56. package/dist/realtime/registry.js.map +1 -1
  57. package/dist/routes/realtime.d.ts +5 -1
  58. package/dist/routes/realtime.js +16 -9
  59. package/dist/routes/realtime.js.map +1 -1
  60. package/dist/test/integration/account-deletion.test.js +2 -2
  61. package/dist/test/integration/account-deletion.test.js.map +1 -1
  62. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  63. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  64. package/dist/test/integration/auth/current-password.test.js +2 -1
  65. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  66. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  67. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  68. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  69. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  70. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  71. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  72. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  73. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  74. package/dist/test/integration/email-change.test.js +2 -1
  75. package/dist/test/integration/email-change.test.js.map +1 -1
  76. package/dist/test/integration/files/component-files.test.js +2 -2
  77. package/dist/test/integration/files/component-files.test.js.map +1 -1
  78. package/dist/test/integration/member-settings-access.test.js +8 -7
  79. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  80. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  81. package/dist/test/integration/menu-access-events.test.js +148 -0
  82. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  83. package/dist/test/integration/notification-inbox.test.js +4 -1
  84. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  85. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  86. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  87. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  88. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  89. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  90. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  91. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  92. package/dist/test/integration/permission-warnings.test.js +3 -2
  93. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  94. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  95. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  96. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  97. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  98. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  99. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  100. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  101. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  102. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  103. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  104. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  105. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  106. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  107. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  108. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  109. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  110. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  111. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  112. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  113. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  114. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  115. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  116. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  117. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  118. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  120. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  121. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  122. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  123. package/frontend-vue/deferred-plugins.ts +3 -4
  124. package/frontend-vue/dms.email.ts +5 -1
  125. package/frontend-vue/dms.frontend.ts +15 -5
  126. package/frontend-vue/dms.server.ts +74 -0
  127. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  128. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  129. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  130. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  131. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  136. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  137. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  138. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  139. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  140. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  141. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  142. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  143. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  144. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  145. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  146. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  148. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  150. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  151. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  152. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  153. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  154. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  155. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  156. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  157. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  158. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  159. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  160. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  161. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  162. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  163. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  164. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  165. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  166. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  169. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  170. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  171. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  172. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  179. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  180. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  181. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  182. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  183. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  184. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  185. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  186. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  187. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  191. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  192. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  193. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  194. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  195. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  196. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  197. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  198. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  199. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  200. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  201. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  202. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  203. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  204. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  205. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  206. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  207. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  208. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  209. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  210. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  211. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  212. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  213. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  214. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  215. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  216. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  217. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  218. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  219. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  220. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  221. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  222. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  223. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  224. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  225. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  226. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  227. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  228. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  230. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  231. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  232. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  233. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  234. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  235. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  238. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  239. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  240. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  241. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  244. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  245. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  246. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  247. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  248. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  249. package/frontend-vue/package.json +2 -2
  250. package/frontend-vue/pnpm-lock.yaml +5 -5
  251. package/frontend-vue/server/private-options.ts +27 -0
  252. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  253. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  254. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  255. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  256. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  257. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  258. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  259. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  260. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  261. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  262. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  263. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  264. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  265. package/frontend-vue/tests/email-layout.test.ts +66 -4
  266. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  267. package/frontend-vue/tests/favicon.test.ts +131 -0
  268. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  269. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  270. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  271. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  272. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  273. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  274. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  275. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  276. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  277. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  278. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  279. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  280. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  281. package/frontend-vue/tests/module-routing.test.ts +2 -5
  282. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  283. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  284. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  285. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  286. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  287. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  288. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  289. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  290. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  291. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  292. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  293. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  294. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  295. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  296. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  297. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  298. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  299. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  300. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  301. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  302. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  303. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  304. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  305. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  306. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  307. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  308. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  309. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  310. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  311. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  312. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  313. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  314. package/frontend-vue/tests/server-routes.test.ts +582 -0
  315. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  316. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  317. package/frontend-vue/tests/side-panels.test.ts +8 -1
  318. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  319. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  320. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  321. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  322. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  323. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  324. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  325. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  326. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  327. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  328. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  329. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  330. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  331. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  332. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  333. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  334. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  335. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  336. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  337. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  338. package/frontend-vue/tests/user-language.test.ts +8 -5
  339. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  340. package/frontend-vue/vitest.config.ts +6 -0
  341. package/package.json +2 -2
  342. package/skills/dms/SKILL.md +2 -1
  343. package/skills/dms-module/SKILL.md +1 -1
  344. package/skills/dms-pages/SKILL.md +4 -1
  345. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  346. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  347. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  348. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  349. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  350. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  351. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -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
+ }
@@ -0,0 +1,251 @@
1
+ import type { DmsServerEvent } from "#dms/server";
2
+ import {
3
+ isPersistent,
4
+ persistAccountSession,
5
+ readAccount,
6
+ removeAccount,
7
+ storeAccount,
8
+ type StoredAccount,
9
+ } from "./accounts";
10
+ import { isRefusal, type SessionTokens, sessionFrom } from "./session";
11
+
12
+ // Carries the "keep me signed in" choice of a login across its 2FA step, whose
13
+ // request names only the challenge. Lives as long as the challenge token.
14
+ const PENDING_LOGIN = "dms_pending_login";
15
+ const PENDING_LOGIN_PATH = "/auth/verify-2fa";
16
+ const PENDING_LOGIN_LIFETIME_S = 300;
17
+
18
+ // Absolute backend API paths only: no scheme, no authority, no query string,
19
+ // and no segment that could climb out of `/api/`.
20
+ const BACKEND_API_PATH =
21
+ /^\/api\/[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9][A-Za-z0-9._~-]*)*$/;
22
+
23
+ type Input = Record<string, unknown>;
24
+
25
+ interface EstablishResult extends SessionTokens {
26
+ requires_2fa?: boolean;
27
+ requires_tenant_assignment?: boolean;
28
+ two_factor_token?: string;
29
+ }
30
+
31
+ interface PendingLogin {
32
+ token: string;
33
+ persistent: boolean;
34
+ }
35
+
36
+ /**
37
+ * Calls a backend endpoint that opens a session and, unless it asks for a
38
+ * second step, writes the session and stores the account.
39
+ */
40
+ async function establishWith(
41
+ event: DmsServerEvent,
42
+ endpoint: string,
43
+ payload: unknown,
44
+ persistent = true,
45
+ ) {
46
+ const result = await event.backend<EstablishResult>(endpoint, {
47
+ method: "POST",
48
+ body: payload,
49
+ });
50
+ if (result.requires_2fa && typeof result.two_factor_token === "string")
51
+ event.cookies.writeSealed(
52
+ PENDING_LOGIN,
53
+ { token: result.two_factor_token, persistent },
54
+ { maxAge: PENDING_LOGIN_LIFETIME_S, path: PENDING_LOGIN_PATH },
55
+ );
56
+ if (result.requires_2fa || result.requires_tenant_assignment) return result;
57
+ if (event.cookies.readSealed(PENDING_LOGIN))
58
+ event.cookies.clear(PENDING_LOGIN, { path: PENDING_LOGIN_PATH });
59
+ const account = persistAccountSession(event, sessionFrom(result, persistent));
60
+ return { user: result.user, account };
61
+ }
62
+
63
+ /**
64
+ * `POST /auth/login`: signs in with a password. Only a login that asked to be
65
+ * kept signed in outlives the browser; any other ends when the browser closes.
66
+ */
67
+ export async function login(event: DmsServerEvent) {
68
+ const input = await event.readJson<Input>();
69
+ return establishWith(
70
+ event,
71
+ "/api/auth/login",
72
+ input,
73
+ input.keep_login === true,
74
+ );
75
+ }
76
+
77
+ /** `POST /auth/register`: the signup page itself is `/auth/signup`. */
78
+ export async function signup(event: DmsServerEvent) {
79
+ return establishWith(event, "/api/auth/signup", await event.readJson());
80
+ }
81
+
82
+ /**
83
+ * `POST /auth/verify-2fa`: finishes a login on its 2FA step, with the lifetime
84
+ * the login chose. A challenge that no login of this browser left a choice
85
+ * for, such as the one an OAuth sign-in hands over, keeps the default.
86
+ */
87
+ export async function verifyTwoFactor(event: DmsServerEvent) {
88
+ const input = await event.readJson<Input>();
89
+ const pending = event.cookies.readSealed<PendingLogin>(PENDING_LOGIN);
90
+ const persistent =
91
+ typeof input.token === "string" && pending?.token === input.token
92
+ ? isPersistent(pending)
93
+ : true;
94
+ return establishWith(event, "/api/auth/verify-2fa", input, persistent);
95
+ }
96
+
97
+ /** `POST /auth/request-2fa-email`: relayed to the backend as is. */
98
+ export async function requestTwoFactorEmail(event: DmsServerEvent) {
99
+ return event.backend("/api/auth/request-2fa-email", {
100
+ method: "POST",
101
+ body: await event.readJson(),
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Whether a caller-named endpoint is one of the declared ones. The grammar is
107
+ * checked on the caller's value too, so a declaration that was never meant to
108
+ * be a prefix cannot be widened by a traversal or a query string smuggled
109
+ * into the request.
110
+ */
111
+ export function isAllowedEstablishEndpoint(
112
+ endpoint: unknown,
113
+ allowed: readonly string[],
114
+ ): endpoint is string {
115
+ return (
116
+ typeof endpoint === "string" &&
117
+ BACKEND_API_PATH.test(endpoint) &&
118
+ allowed.includes(endpoint)
119
+ );
120
+ }
121
+
122
+ /**
123
+ * `POST /auth/establish`: opens a session from a backend endpoint that mints a
124
+ * token pair, the generic half of `/auth/login`. A module whose own flow ends
125
+ * in an authenticated user (a registration completing, an invitation being
126
+ * redeemed) declares the backend route that finishes it
127
+ * (`authEstablishEndpoints`, or `DMS_AUTH_ESTABLISH_ENDPOINTS`); the browser
128
+ * names it and a payload, and never carries a token.
129
+ */
130
+ export async function establish(event: DmsServerEvent) {
131
+ const input = await event.readJson<Input>();
132
+ if (
133
+ !isAllowedEstablishEndpoint(
134
+ input.endpoint,
135
+ event.app.authEstablishEndpoints,
136
+ )
137
+ )
138
+ return Response.json({ error: "Forbidden" }, { status: 403 });
139
+ return establishWith(event, input.endpoint, input.payload ?? {});
140
+ }
141
+
142
+ /** `POST /auth/remove-account`: forgets a stored account. */
143
+ export async function removeStoredAccount(event: DmsServerEvent) {
144
+ const input = await event.readJson<Input>();
145
+ removeAccount(event, input.accountId);
146
+ return { success: true };
147
+ }
148
+
149
+ /** `POST /auth/switch-tenant`: the same account, in another tenant. */
150
+ export async function switchTenant(event: DmsServerEvent) {
151
+ const session = event.session.read();
152
+ const input = await event.readJson<Input>();
153
+ if (!session)
154
+ return Response.json({ message: "Not authenticated" }, { status: 401 });
155
+ if (!input.tenantId)
156
+ return Response.json({ message: "tenantId required" }, { status: 400 });
157
+ const tokens = await event.backend<SessionTokens>("/api/auth/switch-tenant", {
158
+ method: "POST",
159
+ token: session.accessToken,
160
+ body: { refreshToken: session.refreshToken, tenantId: input.tenantId },
161
+ });
162
+ const user = await event.backend<SessionTokens["user"]>("/api/auth/me", {
163
+ token: tokens.access_token,
164
+ });
165
+ const switched = sessionFrom({ ...tokens, user }, isPersistent(session));
166
+ persistAccountSession(event, switched, session.accountId);
167
+ return { success: true, user };
168
+ }
169
+
170
+ /** `POST /auth/switch-account`: signs in as another stored account. */
171
+ export async function switchAccount(event: DmsServerEvent) {
172
+ const input = await event.readJson<Input>();
173
+ const account = readAccount(event, input.accountId);
174
+ if (!account)
175
+ return Response.json({ message: "Account expired" }, { status: 401 });
176
+ try {
177
+ const tokens = await event.backend<SessionTokens>("/api/auth/refresh", {
178
+ method: "POST",
179
+ body: { token: account.refreshToken },
180
+ });
181
+ const user = await event.backend<SessionTokens["user"]>("/api/auth/me", {
182
+ token: tokens.access_token,
183
+ });
184
+ const session = sessionFrom({ ...tokens, user }, isPersistent(account));
185
+ const descriptor = persistAccountSession(
186
+ event,
187
+ session,
188
+ input.accountId as string,
189
+ );
190
+ return { success: true, user, account: descriptor };
191
+ } catch (error) {
192
+ if (isRefusal(error)) removeAccount(event, input.accountId);
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Keep the tokens a validation refreshed, without changing which account is
199
+ * signed in. Another account only has its own cookie updated. The current one
200
+ * also has the session updated, since a refresh may have rotated the token the
201
+ * session holds; it keeps its user as the backend gave it.
202
+ */
203
+ function storeValidatedAccount(
204
+ event: DmsServerEvent,
205
+ account: StoredAccount,
206
+ accountId: string,
207
+ tokens: SessionTokens,
208
+ ) {
209
+ const current = event.session.read();
210
+ if (current?.accountId === accountId) {
211
+ persistAccountSession(
212
+ event,
213
+ sessionFrom({ ...tokens, user: current.user }, isPersistent(current)),
214
+ accountId,
215
+ );
216
+ return;
217
+ }
218
+ const user = {
219
+ _id: account.userId,
220
+ email: account.email,
221
+ name: account.name,
222
+ };
223
+ storeAccount(
224
+ event,
225
+ sessionFrom({ ...tokens, user }, isPersistent(account)),
226
+ accountId,
227
+ );
228
+ }
229
+
230
+ /**
231
+ * `POST /auth/validate-account`: whether a stored account can still sign in,
232
+ * keeping the tokens the check refreshed. `null` when the backend could not
233
+ * tell.
234
+ */
235
+ export async function validateAccount(event: DmsServerEvent) {
236
+ const input = await event.readJson<Input>();
237
+ const account = readAccount(event, input.accountId);
238
+ if (!account) return { valid: false };
239
+ try {
240
+ const tokens = await event.backend<SessionTokens>("/api/auth/refresh", {
241
+ method: "POST",
242
+ body: { token: account.refreshToken },
243
+ });
244
+ storeValidatedAccount(event, account, input.accountId as string, tokens);
245
+ return { valid: true };
246
+ } catch (error) {
247
+ const rejected = isRefusal(error);
248
+ if (rejected) removeAccount(event, input.accountId);
249
+ return { valid: rejected ? false : null };
250
+ }
251
+ }