@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.
- package/CHANGELOG.md +20 -0
- package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
- package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
- package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
- package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
- package/dist/implementations/dms/frontend-bootstrap.js +13 -0
- package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.d.ts +4 -2
- package/dist/implementations/dms/frontend-modules.js +8 -7
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/manifest.d.ts +6 -0
- package/dist/implementations/dms/manifest.js +13 -0
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/implementations/dms/menu-categories.d.ts +16 -0
- package/dist/implementations/dms/menu-categories.js +30 -0
- package/dist/implementations/dms/menu-categories.js.map +1 -0
- package/dist/implementations/dms/nav-badges.d.ts +0 -8
- package/dist/implementations/dms/nav-badges.js +0 -16
- package/dist/implementations/dms/nav-badges.js.map +1 -1
- package/dist/implementations/dms/page.d.ts +11 -5
- package/dist/implementations/dms/page.js +56 -41
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/implementations/dms-html-render/index.js +5 -2
- package/dist/implementations/dms-html-render/index.js.map +1 -1
- package/dist/implementations/dms-notifications/index.d.ts +4 -3
- package/dist/implementations/dms-notifications/index.js +14 -6
- package/dist/implementations/dms-notifications/index.js.map +1 -1
- package/dist/pages/settings/settings-index.js +3 -2
- package/dist/pages/settings/settings-index.js.map +1 -1
- package/dist/pages/settings/users/invites.js +2 -2
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/member-change-notifications.js +17 -6
- package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
- package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
- package/dist/pages/settings/users/member-invite-form.js +7 -4
- package/dist/pages/settings/users/member-invite-form.js.map +1 -1
- package/dist/pages/settings/users/member-roles-field.js +2 -1
- package/dist/pages/settings/users/member-roles-field.js.map +1 -1
- package/dist/pages/settings/users/members.js +4 -1
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.d.ts +6 -0
- package/dist/pages/settings/users/notifications.js +23 -15
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/pages/settings/users/profile.js +1 -1
- package/dist/pages/settings/users/profile.js.map +1 -1
- package/dist/pages/settings/users/region-form.d.ts +1 -1
- package/dist/pages/settings/users/region-form.js +2 -1
- package/dist/pages/settings/users/region-form.js.map +1 -1
- package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
- package/dist/pages/settings/users/role-editor-store.js +17 -4
- package/dist/pages/settings/users/role-editor-store.js.map +1 -1
- package/dist/pages/settings/users/roles.js +1 -0
- package/dist/pages/settings/users/roles.js.map +1 -1
- package/dist/realtime/registry.d.ts +1 -0
- package/dist/realtime/registry.js +5 -0
- package/dist/realtime/registry.js.map +1 -1
- package/dist/routes/realtime.d.ts +5 -1
- package/dist/routes/realtime.js +16 -9
- package/dist/routes/realtime.js.map +1 -1
- package/dist/test/integration/account-deletion.test.js +2 -2
- package/dist/test/integration/account-deletion.test.js.map +1 -1
- package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
- package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
- package/dist/test/integration/auth/current-password.test.js +2 -1
- package/dist/test/integration/auth/current-password.test.js.map +1 -1
- package/dist/test/integration/auth/session-revocation.test.js +2 -1
- package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
- package/dist/test/integration/auth/sign-in-country.test.js +2 -1
- package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
- package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
- package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
- package/dist/test/integration/auth/two-factor-password.test.js +3 -2
- package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
- package/dist/test/integration/email-change.test.js +2 -1
- package/dist/test/integration/email-change.test.js.map +1 -1
- package/dist/test/integration/files/component-files.test.js +2 -2
- package/dist/test/integration/files/component-files.test.js.map +1 -1
- package/dist/test/integration/member-settings-access.test.js +8 -7
- package/dist/test/integration/member-settings-access.test.js.map +1 -1
- package/dist/test/integration/menu-access-events.test.d.ts +1 -0
- package/dist/test/integration/menu-access-events.test.js +148 -0
- package/dist/test/integration/menu-access-events.test.js.map +1 -0
- package/dist/test/integration/notification-inbox.test.js +4 -1
- package/dist/test/integration/notification-inbox.test.js.map +1 -1
- package/dist/test/integration/notification-locked-subjects.test.js +3 -2
- package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
- package/dist/test/integration/notification-unread-badge.test.js +51 -5
- package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
- package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
- package/dist/test/integration/page-layout-route-guard.test.js +56 -0
- package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
- package/dist/test/integration/permission-warnings.test.js +3 -2
- package/dist/test/integration/permission-warnings.test.js.map +1 -1
- package/dist/test/integration/removed-member-notifications.test.js +3 -2
- package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
- package/dist/test/integration/role-delete-reassign.test.js +2 -1
- package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
- package/dist/test/integration/role-permission-ancestors.test.js +2 -1
- package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
- package/dist/test/unit/frontend-bootstrap.test.js +13 -0
- package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
- package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
- package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
- package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
- package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
- package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
- package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
- package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
- package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
- package/dist/test/unit/realtime/menu-topics.test.js +26 -0
- package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
- package/frontend-vue/deferred-plugins.ts +3 -4
- package/frontend-vue/dms.email.ts +5 -1
- package/frontend-vue/dms.frontend.ts +15 -5
- package/frontend-vue/dms.server.ts +74 -0
- package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
- package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
- package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
- package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
- package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
- package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
- package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
- package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
- package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
- package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
- package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
- package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
- package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
- package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
- package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
- package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
- package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
- package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
- package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
- package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
- package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
- package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
- package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
- package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
- package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
- package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
- package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
- package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
- package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
- package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
- package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
- package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
- package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
- package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
- package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
- package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
- package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
- package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
- package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
- package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
- package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
- package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
- package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
- package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
- package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
- package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
- package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
- package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
- package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
- package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
- package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
- package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
- package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +5 -5
- package/frontend-vue/server/private-options.ts +27 -0
- package/frontend-vue/tests/async-data-scope.test.ts +121 -0
- package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
- package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
- package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
- package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
- package/frontend-vue/tests/block-not-found.test.ts +16 -2
- package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
- package/frontend-vue/tests/block-route-params.test.ts +3 -0
- package/frontend-vue/tests/branding-defaults.test.ts +34 -0
- package/frontend-vue/tests/builtin-emails.test.ts +23 -2
- package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
- package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
- package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
- package/frontend-vue/tests/email-layout.test.ts +66 -4
- package/frontend-vue/tests/error-page-message.test.ts +25 -2
- package/frontend-vue/tests/favicon.test.ts +131 -0
- package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
- package/frontend-vue/tests/file-read-urls.test.ts +145 -34
- package/frontend-vue/tests/file-unavailable.test.ts +5 -1
- package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
- package/frontend-vue/tests/form-load-error.test.ts +55 -1
- package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
- package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
- package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
- package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
- package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
- package/frontend-vue/tests/member-role-picker.test.ts +4 -2
- package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
- package/frontend-vue/tests/metadata-retry.test.ts +5 -1
- package/frontend-vue/tests/module-routing.test.ts +2 -5
- package/frontend-vue/tests/modules-catalog.test.ts +17 -4
- package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
- package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
- package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
- package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
- package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
- package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
- package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
- package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
- package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
- package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
- package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
- package/frontend-vue/tests/page-fill-height.test.ts +16 -3
- package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
- package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
- package/frontend-vue/tests/page-record-label.test.ts +0 -7
- package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
- package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
- package/frontend-vue/tests/period-live-pause.test.ts +3 -0
- package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
- package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
- package/frontend-vue/tests/period-ssr.test.ts +195 -0
- package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
- package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
- package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
- package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
- package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
- package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
- package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
- package/frontend-vue/tests/row-side-panel.test.ts +1 -1
- package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
- package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
- package/frontend-vue/tests/server-routes.test.ts +582 -0
- package/frontend-vue/tests/session-handoff.test.ts +1 -1
- package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
- package/frontend-vue/tests/side-panels.test.ts +8 -1
- package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
- package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
- package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
- package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
- package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
- package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
- package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
- package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
- package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
- package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
- package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
- package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
- package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
- package/frontend-vue/tests/table-view-views.test.ts +1 -1
- package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
- package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
- package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
- package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
- package/frontend-vue/tests/use-api-data.test.ts +410 -0
- package/frontend-vue/tests/user-language.test.ts +8 -5
- package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
- package/frontend-vue/vitest.config.ts +6 -0
- package/package.json +2 -2
- package/skills/dms/SKILL.md +2 -1
- package/skills/dms-module/SKILL.md +1 -1
- package/skills/dms-pages/SKILL.md +4 -1
- package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
- package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
- package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
- package/frontend-vue/tests/control-error-path.test.ts +0 -11
- package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
- package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { DmsServerEvent } from "#dms/server";
|
|
3
|
+
import { privateSecret } from "../../../server/private-options";
|
|
4
|
+
import { persistAccountSession } from "./accounts";
|
|
5
|
+
import { type SessionTokens, sessionFrom } from "./session";
|
|
6
|
+
|
|
7
|
+
// The provider sign-in, on the frontend's origin: the backend builds the
|
|
8
|
+
// provider's URL and its callback (`relay.ts`, `config.ts`) and checks the
|
|
9
|
+
// relay header this server sends, from the DMS module's private options.
|
|
10
|
+
|
|
11
|
+
const FLOW = "dms_oauth_flow";
|
|
12
|
+
const HANDOFF = "dms_oauth_handoff";
|
|
13
|
+
const FLOW_PATH = "/auth/oauth";
|
|
14
|
+
const COMPLETE = "/auth/oauth/complete";
|
|
15
|
+
const SIGN_IN = "/auth";
|
|
16
|
+
const OAUTH_ERROR = "error.oauth.";
|
|
17
|
+
const FAILED = "error.oauth.failed";
|
|
18
|
+
const FLOW_LIFETIME_MS = 10 * 60 * 1000;
|
|
19
|
+
const HANDOFF_LIFETIME_S = 300;
|
|
20
|
+
const RATE_WINDOW_MS = 60 * 1000;
|
|
21
|
+
const RATE_LIMIT = 10;
|
|
22
|
+
const MAX_RATE_LIMIT_CLIENTS = 10_000;
|
|
23
|
+
const MAX_CALLBACK_PARAMETER_LENGTH = 2048;
|
|
24
|
+
const PROVIDER = /^[a-z0-9-]{1,32}$/;
|
|
25
|
+
|
|
26
|
+
interface RateWindow {
|
|
27
|
+
count: number;
|
|
28
|
+
startedAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface OAuthFlow {
|
|
32
|
+
state: string;
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
redirect: string;
|
|
35
|
+
language: string;
|
|
36
|
+
invite: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface OAuthResult extends SessionTokens {
|
|
40
|
+
requires_2fa?: boolean;
|
|
41
|
+
requires_tenant_assignment?: boolean;
|
|
42
|
+
two_factor_token?: string;
|
|
43
|
+
tenant_assignment_token?: string;
|
|
44
|
+
methods?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Process-local, like the session refresh: one frontend process per sticky
|
|
48
|
+
// client. A development reload of this file starts it over.
|
|
49
|
+
const attempts = new Map<string, RateWindow>();
|
|
50
|
+
|
|
51
|
+
function sweepAttempts(now: number) {
|
|
52
|
+
for (const [key, window] of attempts)
|
|
53
|
+
if (now - window.startedAt >= RATE_WINDOW_MS) attempts.delete(key);
|
|
54
|
+
while (attempts.size >= MAX_RATE_LIMIT_CLIENTS)
|
|
55
|
+
attempts.delete(attempts.keys().next().value as string);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Counts an attempt for `key`; false once over the limit of the window. */
|
|
59
|
+
export function hitOAuthRateLimitKey(key: string, now = Date.now()): boolean {
|
|
60
|
+
sweepAttempts(now);
|
|
61
|
+
const window = attempts.get(key);
|
|
62
|
+
if (!window || now - window.startedAt >= RATE_WINDOW_MS) {
|
|
63
|
+
attempts.set(key, { count: 1, startedAt: now });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
window.count += 1;
|
|
67
|
+
return window.count <= RATE_LIMIT;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function rateLimited(event: DmsServerEvent, scope: string): boolean {
|
|
71
|
+
return !hitOAuthRateLimitKey(`${scope}:${event.clientIp()}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isValidOAuthProvider(provider: unknown): provider is string {
|
|
75
|
+
return typeof provider === "string" && PROVIDER.test(provider);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function safePath(value: string | null): string {
|
|
79
|
+
if (
|
|
80
|
+
typeof value !== "string" ||
|
|
81
|
+
!value.startsWith("/") ||
|
|
82
|
+
value.startsWith("//") ||
|
|
83
|
+
value.includes("\\")
|
|
84
|
+
)
|
|
85
|
+
return "";
|
|
86
|
+
try {
|
|
87
|
+
const decoded = decodeURIComponent(value);
|
|
88
|
+
return decoded.startsWith("//") || decoded.includes("\\") ? "" : value;
|
|
89
|
+
} catch {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function redirect(location: string): Response {
|
|
95
|
+
return new Response(null, { status: 302, headers: { location } });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function errorRedirect(key: string): Response {
|
|
99
|
+
return redirect(`${SIGN_IN}?oauth_error=${encodeURIComponent(key)}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The backend's own OAuth key says why a provider sign-in failed; any other
|
|
103
|
+
// failure keeps the generic one.
|
|
104
|
+
function failureRedirect(error: unknown): Response {
|
|
105
|
+
const message = error instanceof Error ? error.message : "";
|
|
106
|
+
const status = (error as { status?: unknown }).status;
|
|
107
|
+
return errorRedirect(
|
|
108
|
+
typeof status === "number" && message.startsWith(OAUTH_ERROR)
|
|
109
|
+
? message
|
|
110
|
+
: FAILED,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function relayHeaders(event: DmsServerEvent): Record<string, string> {
|
|
115
|
+
const secret = privateSecret(
|
|
116
|
+
event,
|
|
117
|
+
"oauth",
|
|
118
|
+
"relaySecret",
|
|
119
|
+
"DMS_OAUTH_RELAY_SECRET",
|
|
120
|
+
);
|
|
121
|
+
return { "x-dms-oauth-relay": secret ?? "" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `GET /auth/oauth/:provider/start`: on to the provider's sign-in page. */
|
|
125
|
+
export async function oauthStart(event: DmsServerEvent): Promise<Response> {
|
|
126
|
+
const { provider } = event.params;
|
|
127
|
+
if (
|
|
128
|
+
!isValidOAuthProvider(provider) ||
|
|
129
|
+
event.request.headers.get("sec-fetch-site") === "cross-site" ||
|
|
130
|
+
rateLimited(event, "start")
|
|
131
|
+
)
|
|
132
|
+
return errorRedirect(FAILED);
|
|
133
|
+
try {
|
|
134
|
+
const result = await event.backend<{
|
|
135
|
+
state: string;
|
|
136
|
+
authorizeUrl: string;
|
|
137
|
+
}>(`/api/auth/oauth/${provider}/authorize-url`, {
|
|
138
|
+
headers: relayHeaders(event),
|
|
139
|
+
});
|
|
140
|
+
const query = event.url.searchParams;
|
|
141
|
+
event.cookies.writeSealed(
|
|
142
|
+
FLOW,
|
|
143
|
+
{
|
|
144
|
+
state: result.state,
|
|
145
|
+
expiresAt: Date.now() + FLOW_LIFETIME_MS,
|
|
146
|
+
redirect: safePath(query.get("redirect")),
|
|
147
|
+
language: query.get("language") ?? "",
|
|
148
|
+
invite: query.get("invite") ?? "",
|
|
149
|
+
} satisfies OAuthFlow,
|
|
150
|
+
{ maxAge: FLOW_LIFETIME_MS / 1000, path: FLOW_PATH },
|
|
151
|
+
);
|
|
152
|
+
return redirect(result.authorizeUrl);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return failureRedirect(error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sameState(left: unknown, right: unknown): boolean {
|
|
159
|
+
if (typeof left !== "string" || typeof right !== "string") return false;
|
|
160
|
+
const a = Buffer.from(left);
|
|
161
|
+
const b = Buffer.from(right);
|
|
162
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validCallback(flow: OAuthFlow | undefined, query: URLSearchParams) {
|
|
166
|
+
const code = query.get("code");
|
|
167
|
+
const state = query.get("state");
|
|
168
|
+
return Boolean(
|
|
169
|
+
flow &&
|
|
170
|
+
flow.expiresAt >= Date.now() &&
|
|
171
|
+
typeof code === "string" &&
|
|
172
|
+
code.length <= MAX_CALLBACK_PARAMETER_LENGTH &&
|
|
173
|
+
typeof state === "string" &&
|
|
174
|
+
state.length <= MAX_CALLBACK_PARAMETER_LENGTH &&
|
|
175
|
+
sameState(state, flow.state),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function storeOAuthResult(
|
|
180
|
+
event: DmsServerEvent,
|
|
181
|
+
provider: string,
|
|
182
|
+
result: OAuthResult,
|
|
183
|
+
) {
|
|
184
|
+
const handoff = { maxAge: HANDOFF_LIFETIME_S, path: FLOW_PATH };
|
|
185
|
+
if (result.requires_2fa)
|
|
186
|
+
event.cookies.writeSealed(
|
|
187
|
+
HANDOFF,
|
|
188
|
+
{ kind: "2fa", token: result.two_factor_token, methods: result.methods },
|
|
189
|
+
handoff,
|
|
190
|
+
);
|
|
191
|
+
else if (result.requires_tenant_assignment)
|
|
192
|
+
event.cookies.writeSealed(
|
|
193
|
+
HANDOFF,
|
|
194
|
+
{
|
|
195
|
+
kind: "no-workspace",
|
|
196
|
+
token: result.tenant_assignment_token,
|
|
197
|
+
provider,
|
|
198
|
+
},
|
|
199
|
+
handoff,
|
|
200
|
+
);
|
|
201
|
+
else persistAccountSession(event, sessionFrom(result));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function completeOAuth(
|
|
205
|
+
event: DmsServerEvent,
|
|
206
|
+
provider: string,
|
|
207
|
+
flow: OAuthFlow,
|
|
208
|
+
): Promise<Response> {
|
|
209
|
+
const query = event.url.searchParams;
|
|
210
|
+
try {
|
|
211
|
+
const result = await event.backend<OAuthResult>(
|
|
212
|
+
`/api/auth/oauth/${provider}/callback`,
|
|
213
|
+
{
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: relayHeaders(event),
|
|
216
|
+
body: {
|
|
217
|
+
code: query.get("code"),
|
|
218
|
+
state: query.get("state"),
|
|
219
|
+
state_cookie: flow.state,
|
|
220
|
+
language: flow.language || undefined,
|
|
221
|
+
invite: flow.invite || undefined,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
storeOAuthResult(event, provider, result);
|
|
226
|
+
return redirect(
|
|
227
|
+
flow.redirect &&
|
|
228
|
+
!result.requires_2fa &&
|
|
229
|
+
!result.requires_tenant_assignment
|
|
230
|
+
? `${COMPLETE}?redirect=${encodeURIComponent(flow.redirect)}`
|
|
231
|
+
: COMPLETE,
|
|
232
|
+
);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return failureRedirect(error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `GET /auth/oauth/:provider/callback`: back from the provider. */
|
|
239
|
+
export async function oauthCallback(event: DmsServerEvent): Promise<Response> {
|
|
240
|
+
const { provider } = event.params;
|
|
241
|
+
if (!isValidOAuthProvider(provider) || rateLimited(event, "callback"))
|
|
242
|
+
return errorRedirect(FAILED);
|
|
243
|
+
const flow = event.cookies.readSealed<OAuthFlow>(FLOW);
|
|
244
|
+
event.cookies.clear(FLOW, { path: FLOW_PATH });
|
|
245
|
+
const query = event.url.searchParams;
|
|
246
|
+
if (query.has("error")) return errorRedirect("error.oauth.cancelled");
|
|
247
|
+
if (!flow || !validCallback(flow, query))
|
|
248
|
+
return errorRedirect("error.oauth.invalid_state");
|
|
249
|
+
return completeOAuth(event, provider, flow);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* `POST /auth/oauth/handoff`: what the provider sign-in left for the page to
|
|
254
|
+
* finish (a 2FA step, a workspace to pick), once.
|
|
255
|
+
*/
|
|
256
|
+
export function oauthHandoff(event: DmsServerEvent) {
|
|
257
|
+
const handoff = event.cookies.readSealed(HANDOFF) ?? { kind: "none" };
|
|
258
|
+
event.cookies.clear(HANDOFF, { path: FLOW_PATH });
|
|
259
|
+
return handoff;
|
|
260
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { DmsPublicSession, DmsServerEvent, DmsSession } from "#dms/server";
|
|
3
|
+
import { isPersistent, persistAccountSession, removeAccount } from "./accounts";
|
|
4
|
+
|
|
5
|
+
const REFRESH_TTL_MS = 15_000;
|
|
6
|
+
const REFUSED = [400, 401, 403];
|
|
7
|
+
|
|
8
|
+
/** A backend answer that opens a session: a token pair and the user. */
|
|
9
|
+
export interface SessionTokens {
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
user?: DmsSession["user"];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RefreshFlight {
|
|
16
|
+
promise: Promise<SessionTokens>;
|
|
17
|
+
expiresAt?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const refreshes = new Map<string, RefreshFlight>();
|
|
21
|
+
|
|
22
|
+
export function isRefusal(error: unknown): boolean {
|
|
23
|
+
const status = (error as { status?: unknown } | undefined)?.status;
|
|
24
|
+
return typeof status === "number" && REFUSED.includes(status);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The session a token pair opens.
|
|
29
|
+
*
|
|
30
|
+
* @param result Backend answer carrying the tokens and the user
|
|
31
|
+
* @param persistent Whether the session outlives the browser
|
|
32
|
+
*/
|
|
33
|
+
export function sessionFrom(
|
|
34
|
+
result: SessionTokens,
|
|
35
|
+
persistent = true,
|
|
36
|
+
): DmsSession {
|
|
37
|
+
const payload = JSON.parse(
|
|
38
|
+
Buffer.from(
|
|
39
|
+
result.access_token.split(".")[1] ?? "",
|
|
40
|
+
"base64url",
|
|
41
|
+
).toString() || "{}",
|
|
42
|
+
) as { tenantId?: string };
|
|
43
|
+
return {
|
|
44
|
+
user: result.user,
|
|
45
|
+
accessToken: result.access_token,
|
|
46
|
+
refreshToken: result.refresh_token,
|
|
47
|
+
activeTenantId: payload.tenantId ?? "default",
|
|
48
|
+
persistent,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What a signed-in session shows the browser; the server adds `updatedAt`. */
|
|
53
|
+
export function publicSession(session: DmsSession): DmsPublicSession {
|
|
54
|
+
return {
|
|
55
|
+
user: session.user,
|
|
56
|
+
session: {
|
|
57
|
+
accountId: session.accountId,
|
|
58
|
+
activeTenantId: session.activeTenantId,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One backend refresh per refresh token at a time: the requests a page fires
|
|
65
|
+
* together after its access token expired share it, rather than spending the
|
|
66
|
+
* rotating refresh token several times over. Kept a while after it settles
|
|
67
|
+
* for the requests that were already on their way.
|
|
68
|
+
*/
|
|
69
|
+
function singleFlight(
|
|
70
|
+
token: string,
|
|
71
|
+
operation: () => Promise<SessionTokens>,
|
|
72
|
+
): Promise<SessionTokens> {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const [key, entry] of refreshes)
|
|
75
|
+
if (entry.expiresAt && entry.expiresAt <= now) refreshes.delete(key);
|
|
76
|
+
const key = createHash("sha256").update(token).digest("base64url");
|
|
77
|
+
const current = refreshes.get(key);
|
|
78
|
+
if (current) return current.promise;
|
|
79
|
+
const entry: RefreshFlight = { promise: Promise.resolve().then(operation) };
|
|
80
|
+
refreshes.set(key, entry);
|
|
81
|
+
const retain = () => {
|
|
82
|
+
entry.expiresAt = Date.now() + REFRESH_TTL_MS;
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (refreshes.get(key) === entry) refreshes.delete(key);
|
|
85
|
+
}, REFRESH_TTL_MS).unref();
|
|
86
|
+
};
|
|
87
|
+
entry.promise.then(retain, retain);
|
|
88
|
+
return entry.promise;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Renews `session` with its refresh token, writes it and returns it; the rest
|
|
93
|
+
* of the request runs with it. A refused refresh signs the browser out.
|
|
94
|
+
* The frontend server calls it when the backend refuses a signed-in page
|
|
95
|
+
* visit, and `POST /api/_auth/session` when the browser asks.
|
|
96
|
+
*/
|
|
97
|
+
export async function renewSession(
|
|
98
|
+
event: DmsServerEvent,
|
|
99
|
+
session: DmsSession | undefined = event.session.read(),
|
|
100
|
+
): Promise<DmsSession | undefined> {
|
|
101
|
+
const refreshToken = session?.refreshToken;
|
|
102
|
+
if (!session || !refreshToken) return undefined;
|
|
103
|
+
try {
|
|
104
|
+
const tokens = await singleFlight(refreshToken, () =>
|
|
105
|
+
event.backend<SessionTokens>("/api/auth/refresh", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
token: session.accessToken,
|
|
108
|
+
body: { token: refreshToken },
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
const user = await event.backend<DmsSession["user"]>("/api/auth/me", {
|
|
112
|
+
token: tokens.access_token,
|
|
113
|
+
});
|
|
114
|
+
const refreshed = sessionFrom({ ...tokens, user }, isPersistent(session));
|
|
115
|
+
persistAccountSession(event, refreshed, session.accountId);
|
|
116
|
+
event.session.useForRequest(refreshed);
|
|
117
|
+
return refreshed;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (isRefusal(error)) event.session.clear();
|
|
120
|
+
event.session.useForRequest(undefined);
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `GET /api/_auth/session`: the session as the browser sees it. */
|
|
126
|
+
export function readSessionRoute(event: DmsServerEvent) {
|
|
127
|
+
return event.session.view();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `POST /api/_auth/session`: renews the session, then shows it. */
|
|
131
|
+
export async function refreshSessionRoute(event: DmsServerEvent) {
|
|
132
|
+
await renewSession(event);
|
|
133
|
+
return event.session.view();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `DELETE /api/_auth/session`: signs out, on the backend too. */
|
|
137
|
+
export async function logoutRoute(event: DmsServerEvent) {
|
|
138
|
+
const session = event.session.read();
|
|
139
|
+
if (session?.refreshToken && session.accessToken)
|
|
140
|
+
await event
|
|
141
|
+
.backend("/api/auth/logout", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
token: session.accessToken,
|
|
144
|
+
body: { token: session.refreshToken },
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {});
|
|
147
|
+
event.session.clear();
|
|
148
|
+
if (session?.accountId) removeAccount(event, session.accountId);
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Any other method on `/api/_auth/session`. */
|
|
153
|
+
export function sessionMethodNotAllowed() {
|
|
154
|
+
return Response.json(
|
|
155
|
+
{ error: "Method Not Allowed" },
|
|
156
|
+
{ status: 405, headers: { allow: "GET, POST, DELETE" } },
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** The part of the session the data scope follows. */
|
|
2
|
+
export interface ScopedSession {
|
|
3
|
+
accountId?: string;
|
|
4
|
+
activeTenantId?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const HASH_SEED_LOW = 0xdeadbeef;
|
|
8
|
+
const HASH_SEED_HIGH = 0x41c6ce57;
|
|
9
|
+
const HASH_MULTIPLIER_LOW = 2_654_435_761;
|
|
10
|
+
const HASH_MULTIPLIER_HIGH = 1_597_334_677;
|
|
11
|
+
const HASH_MIX_LOW = 2_246_822_507;
|
|
12
|
+
const HASH_MIX_HIGH = 3_266_489_909;
|
|
13
|
+
const HIGH_WORD = 2 ** 32;
|
|
14
|
+
const HIGH_BITS_MASK = 2_097_151;
|
|
15
|
+
const RADIX = 36;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A 53-bit digest of `text` (cyrb53): the same in a server render and in the
|
|
19
|
+
* browser, and synchronous, so the scope is set before the page renders.
|
|
20
|
+
* Not a secret: it only keeps the identifiers out of the page.
|
|
21
|
+
*/
|
|
22
|
+
function digest(text: string): string {
|
|
23
|
+
let low = HASH_SEED_LOW;
|
|
24
|
+
let high = HASH_SEED_HIGH;
|
|
25
|
+
for (let index = 0; index < text.length; index++) {
|
|
26
|
+
const code = text.charCodeAt(index);
|
|
27
|
+
low = Math.imul(low ^ code, HASH_MULTIPLIER_LOW);
|
|
28
|
+
high = Math.imul(high ^ code, HASH_MULTIPLIER_HIGH);
|
|
29
|
+
}
|
|
30
|
+
low = Math.imul(low ^ (low >>> 16), HASH_MIX_LOW);
|
|
31
|
+
low ^= Math.imul(high ^ (high >>> 13), HASH_MIX_HIGH);
|
|
32
|
+
high = Math.imul(high ^ (high >>> 16), HASH_MIX_LOW);
|
|
33
|
+
high ^= Math.imul(low ^ (low >>> 13), HASH_MIX_HIGH);
|
|
34
|
+
return (HIGH_WORD * (HIGH_BITS_MASK & high) + (low >>> 0)).toString(RADIX);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The scope the async data of a session is kept under: a digest of its
|
|
39
|
+
* account and workspace, or none when signed out. The same key read by
|
|
40
|
+
* another account, or in another workspace, is another entry.
|
|
41
|
+
*/
|
|
42
|
+
export function asyncDataScopeOf(
|
|
43
|
+
session: ScopedSession | null | undefined,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
if (!session?.accountId) return undefined;
|
|
46
|
+
return digest(`${session.accountId}\n${session.activeTenantId ?? ""}`);
|
|
47
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
|
|
1
2
|
// "Preview as role" (roles editor): the shapes the preview endpoint answers
|
|
2
3
|
// with, how a preview session travels from the editor tab to the preview tab,
|
|
3
4
|
// and which requests a preview tab may still send. Pure: no Vue, no window.
|
|
@@ -9,7 +10,7 @@ export type PermissionPreviewBlock =
|
|
|
9
10
|
|
|
10
11
|
export type PermissionPreviewState = PermissionPreviewBlock["state"];
|
|
11
12
|
|
|
12
|
-
/** Answer of `POST /settings/workspace/roles/preview`. */
|
|
13
|
+
/** Answer of `POST /dms/pages/settings/workspace/roles/preview`. */
|
|
13
14
|
export interface PermissionPreviewResult {
|
|
14
15
|
page: { fullId: string; displayName: string; hidden: boolean } | null;
|
|
15
16
|
/** Blocks keyed by layout path (component key, then child ids). */
|
|
@@ -44,7 +45,10 @@ export interface PermissionPreviewSession {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/** Endpoint the preview tab asks; a POST, so the request guard lets it through. */
|
|
47
|
-
export const PERMISSION_PREVIEW_ENDPOINT =
|
|
48
|
+
export const PERMISSION_PREVIEW_ENDPOINT = pageRoutePath(
|
|
49
|
+
"/settings/workspace/roles",
|
|
50
|
+
"/preview",
|
|
51
|
+
);
|
|
48
52
|
/** Query parameter that hands a preview session to a new tab. */
|
|
49
53
|
export const PERMISSION_PREVIEW_QUERY_KEY = "dms-preview";
|
|
50
54
|
/** `localStorage` key prefix of the sessions the editor shares with its preview tabs. */
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DmsAsyncDataKey,
|
|
3
|
+
DmsAsyncDataResult,
|
|
4
|
+
DmsPagedDataResult,
|
|
5
|
+
UseAsyncDataOptions,
|
|
6
|
+
UseDmsPagedDataOptions,
|
|
7
|
+
UseFetchOptions,
|
|
8
|
+
} from "#dms/frontend-module";
|
|
9
|
+
import type { MaybeRefOrGetter } from "vue";
|
|
10
|
+
|
|
11
|
+
/** The authenticated fetch `useAuthFetch` returns. */
|
|
12
|
+
type AuthFetch = ReturnType<typeof useAuthFetch>["$authFetch"];
|
|
13
|
+
|
|
14
|
+
/** What a handler loading API data receives. */
|
|
15
|
+
export interface ApiDataContext {
|
|
16
|
+
/** `useAuthFetch`'s fetch: the session retry, the language and realtime headers. */
|
|
17
|
+
$authFetch: AuthFetch;
|
|
18
|
+
/** Aborted once the result is no longer wanted: pass it to each request. */
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ApiDataHandler<T> = (context: ApiDataContext) => Promise<T>;
|
|
23
|
+
|
|
24
|
+
/** `useDmsFetch`'s options, the fetch function aside: it is `$authFetch`. */
|
|
25
|
+
export type UseApiDataOptions<T> = Omit<UseFetchOptions<T>, "$fetch">;
|
|
26
|
+
|
|
27
|
+
/** A backend URL to read, or nothing to read while it is null or undefined. */
|
|
28
|
+
export type ApiDataRequest = MaybeRefOrGetter<string | null | undefined>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What `useApiData` returns: `data`, `status`, `pending`, `error`, the `key`
|
|
32
|
+
* the data is loaded under, `placeholder` (a reactive request still showing
|
|
33
|
+
* the previous answer), `execute()`, `refresh()` and `clear()`. Usable at
|
|
34
|
+
* once, or awaited until the data is there.
|
|
35
|
+
*/
|
|
36
|
+
export type ApiDataResult<T> = DmsAsyncDataResult<T>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads backend data for a component: the one way a module loads what it
|
|
40
|
+
* renders. A server render waits for it and hands it to the browser with the
|
|
41
|
+
* page, which then does not ask again; the callers of one request share it.
|
|
42
|
+
* Data read before shows at once and is read again in the background on each
|
|
43
|
+
* mount (stale while revalidate).
|
|
44
|
+
*
|
|
45
|
+
* - `useApiData(url, options)` reads one URL, like Nuxt's `useFetch`. The URL,
|
|
46
|
+
* `method`, `query`, `params` and `body` may be refs or getters, read again
|
|
47
|
+
* for each request: the data follows them. A URL that is null or undefined
|
|
48
|
+
* reads nothing until it has a value.
|
|
49
|
+
* - `useApiData(key, handler, options)` loads what the handler returns, like
|
|
50
|
+
* Nuxt's `useAsyncData`: for data made of several requests. The handler
|
|
51
|
+
* receives `$authFetch` and the `signal` to pass to each request.
|
|
52
|
+
*
|
|
53
|
+
* Requests go through `$authFetch` (session retry, language and realtime
|
|
54
|
+
* headers), and the data is kept per account and workspace: a switch of
|
|
55
|
+
* either reads it again. The options are the renderer's:
|
|
56
|
+
*
|
|
57
|
+
* - `lazy` spares a client navigation the wait; `server: false` leaves the
|
|
58
|
+
* data to the browser (for what only the browser knows).
|
|
59
|
+
* - `immediate: false` waits for `execute()`: a dialog or a drawer reads
|
|
60
|
+
* what it shows once it opens.
|
|
61
|
+
* - `refreshInterval: ms` reads again on an interval while the caller shows
|
|
62
|
+
* the data and the page is visible: polling. A ref or a getter changes it,
|
|
63
|
+
* and `0` or `null` stops it; a call from a plugin polls for as long as
|
|
64
|
+
* the application runs.
|
|
65
|
+
* - The page's refreshes (`refreshPageBlocks()`) and the events of its
|
|
66
|
+
* realtime topics read the data again; `refreshOnPageRefresh: false` opts
|
|
67
|
+
* out, for data with refresh triggers of its own.
|
|
68
|
+
* - `data` is a shallow ref: replace the value to change it, or pass
|
|
69
|
+
* `deep: true`.
|
|
70
|
+
*
|
|
71
|
+
* A write stays an `$authFetch` call; once it is done, the page's refresh
|
|
72
|
+
* reads what changed, or `useDmsData(key)` puts its answer in the data in
|
|
73
|
+
* place (the URL form names its key with `key`).
|
|
74
|
+
*/
|
|
75
|
+
export function useApiData<T>(
|
|
76
|
+
url: ApiDataRequest,
|
|
77
|
+
options?: UseApiDataOptions<T>,
|
|
78
|
+
): ApiDataResult<T>;
|
|
79
|
+
export function useApiData<T>(
|
|
80
|
+
key: MaybeRefOrGetter<DmsAsyncDataKey>,
|
|
81
|
+
handler: ApiDataHandler<T>,
|
|
82
|
+
options?: UseAsyncDataOptions<T>,
|
|
83
|
+
): ApiDataResult<T>;
|
|
84
|
+
export function useApiData<T>(
|
|
85
|
+
source: ApiDataRequest,
|
|
86
|
+
handlerOrOptions?: ApiDataHandler<T> | UseApiDataOptions<T>,
|
|
87
|
+
options?: UseAsyncDataOptions<T>,
|
|
88
|
+
): ApiDataResult<T> {
|
|
89
|
+
const { $authFetch } = useAuthFetch();
|
|
90
|
+
if (typeof handlerOrOptions === "function")
|
|
91
|
+
return useDmsAsyncData<T>(
|
|
92
|
+
source,
|
|
93
|
+
({ signal }) => handlerOrOptions({ $authFetch, signal }),
|
|
94
|
+
options,
|
|
95
|
+
);
|
|
96
|
+
return useDmsFetch<T>(source, {
|
|
97
|
+
...handlerOrOptions,
|
|
98
|
+
$fetch: $authFetch as UseFetchOptions<T>["$fetch"],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What a handler reading one page of API data receives. */
|
|
103
|
+
export interface ApiPagedDataContext<TCursor> extends ApiDataContext {
|
|
104
|
+
/** The page to read: `initialCursor` (none by default) for the first one. */
|
|
105
|
+
cursor: TCursor | undefined;
|
|
106
|
+
/** Its position, from `0`. */
|
|
107
|
+
index: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type ApiPagedDataHandler<TPage, TCursor> = (
|
|
111
|
+
context: ApiPagedDataContext<TCursor>,
|
|
112
|
+
) => Promise<TPage>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What `useApiPagedData` returns: `useApiData`'s result over the pages
|
|
116
|
+
* loaded, with `pages`, `hasMore`, `loadingMore` and `loadMore()`.
|
|
117
|
+
*/
|
|
118
|
+
export type ApiPagedDataResult<TPage, TCursor> = DmsPagedDataResult<
|
|
119
|
+
TPage,
|
|
120
|
+
TCursor
|
|
121
|
+
>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reads a backend list page by page under one key: a "load more" button, an
|
|
125
|
+
* infinite list. The handler reads the page at `cursor` through `$authFetch`;
|
|
126
|
+
* `nextCursor(page, pages)` gives the next page's cursor, or `null` at the
|
|
127
|
+
* end. A server render shows the first page, `loadMore()` appends the next
|
|
128
|
+
* one, and a revalidation (a mount, the page's refreshes, the polling)
|
|
129
|
+
* reads every page loaded so far again. Put what filters the list in the
|
|
130
|
+
* key (a ref or a getter for it): a new key starts from its first page.
|
|
131
|
+
*
|
|
132
|
+
* @param key The key, or a ref or getter for it
|
|
133
|
+
* @param handler Reads one page
|
|
134
|
+
* @param options `nextCursor`, `initialCursor` and `useApiData`'s options
|
|
135
|
+
*/
|
|
136
|
+
export function useApiPagedData<TPage, TCursor = unknown>(
|
|
137
|
+
key: MaybeRefOrGetter<DmsAsyncDataKey>,
|
|
138
|
+
handler: ApiPagedDataHandler<TPage, TCursor>,
|
|
139
|
+
options: UseDmsPagedDataOptions<TPage, TCursor>,
|
|
140
|
+
): ApiPagedDataResult<TPage, TCursor> {
|
|
141
|
+
const { $authFetch } = useAuthFetch();
|
|
142
|
+
return useDmsPagedData<TPage, TCursor>(
|
|
143
|
+
key,
|
|
144
|
+
({ cursor, index, signal }) =>
|
|
145
|
+
handler({ $authFetch, cursor, index, signal }),
|
|
146
|
+
options,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { UseFetchOptions } from "#dms/frontend-module";
|
|
2
|
-
|
|
3
1
|
export const CONTENT_LANGUAGE_HEADER = "x-content-language";
|
|
4
2
|
|
|
5
3
|
const RETRY_HEADER = "x-auth-retry";
|
|
@@ -38,7 +36,7 @@ function isUnauthorized(error: unknown): boolean {
|
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
export const useAuthFetch = () => {
|
|
41
|
-
const { loggedIn: isLoggedIn } =
|
|
39
|
+
const { loggedIn: isLoggedIn } = useDmsUserSession();
|
|
42
40
|
const { refreshSession, reconcileSession, redirectToAuth } =
|
|
43
41
|
useSessionRecovery();
|
|
44
42
|
const config = useDmsRuntimeConfig();
|
|
@@ -51,10 +49,12 @@ export const useAuthFetch = () => {
|
|
|
51
49
|
|
|
52
50
|
const baseFetch = $fetch.create({
|
|
53
51
|
baseURL: config.public.dms.baseURL,
|
|
54
|
-
headers: {
|
|
55
|
-
[CONTENT_LANGUAGE_HEADER]: dmsApp.$i18n.locale.value,
|
|
56
|
-
},
|
|
57
52
|
onRequest({ options }) {
|
|
53
|
+
// Read as each request leaves: a `$authFetch` kept across a language
|
|
54
|
+
// change asks in the new language. A header the caller sets wins.
|
|
55
|
+
if (!options.headers.has(CONTENT_LANGUAGE_HEADER)) {
|
|
56
|
+
options.headers.set(CONTENT_LANGUAGE_HEADER, dmsApp.$i18n.locale.value);
|
|
57
|
+
}
|
|
58
58
|
if (realtimeSessionId.value) {
|
|
59
59
|
options.headers.set(REALTIME_SESSION_HEADER, realtimeSessionId.value);
|
|
60
60
|
}
|
|
@@ -117,18 +117,5 @@ export const useAuthFetch = () => {
|
|
|
117
117
|
baseFetch.raw(request, options),
|
|
118
118
|
) as AuthFetch["raw"];
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
url: string | (() => string),
|
|
122
|
-
options?: UseFetchOptions<T>,
|
|
123
|
-
) {
|
|
124
|
-
return useDmsFetch(url, {
|
|
125
|
-
...options,
|
|
126
|
-
$fetch: $authFetch,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
$authFetch,
|
|
132
|
-
useFetchAuth,
|
|
133
|
-
};
|
|
120
|
+
return { $authFetch };
|
|
134
121
|
};
|