@byline/host-tanstack-start 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-shell/chrome/admin-app-bar.d.ts +7 -0
- package/dist/admin-shell/chrome/admin-app-bar.js +10 -2
- package/dist/admin-shell/chrome/optional-i18n-provider.d.ts +29 -0
- package/dist/admin-shell/chrome/optional-i18n-provider.js +24 -0
- package/dist/admin-shell/chrome/session-change-boundary.d.ts +14 -0
- package/dist/admin-shell/chrome/session-change-boundary.js +119 -0
- package/dist/admin-shell/chrome/session-change-boundary.module.js +11 -0
- package/dist/admin-shell/chrome/session-change-boundary.test.d.ts +8 -0
- package/dist/admin-shell/chrome/session-change-boundary_module.css +81 -0
- package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -1
- package/dist/admin-shell/chrome/sign-in-page.js +60 -3
- package/dist/integrations/byline-admin-services.js +7 -1
- package/dist/integrations/client-ip.d.ts +19 -0
- package/dist/integrations/client-ip.js +34 -0
- package/dist/integrations/session-coordination.d.ts +24 -0
- package/dist/integrations/session-coordination.js +85 -0
- package/dist/integrations/session-middleware.d.ts +11 -0
- package/dist/integrations/session-middleware.js +94 -0
- package/dist/integrations/session-request-kind.d.ts +9 -0
- package/dist/integrations/session-request-kind.js +10 -0
- package/dist/integrations/session-request-middleware.d.ts +9 -0
- package/dist/integrations/session-request-middleware.js +7 -0
- package/dist/integrations/sign-in-body.d.ts +12 -0
- package/dist/integrations/sign-in-body.js +52 -0
- package/dist/integrations/sign-in-middleware.d.ts +9 -0
- package/dist/integrations/sign-in-middleware.js +10 -0
- package/dist/routes/create-admin-layout-route.js +46 -31
- package/dist/routes/create-sign-in-route.js +4 -2
- package/dist/server-fns/admin-account/change-password.d.ts +3 -1
- package/dist/server-fns/admin-account/change-password.js +9 -3
- package/dist/server-fns/admin-account/get.d.ts +3 -1
- package/dist/server-fns/admin-account/get.js +4 -1
- package/dist/server-fns/admin-account/index.d.ts +5 -5
- package/dist/server-fns/admin-account/update.d.ts +3 -1
- package/dist/server-fns/admin-account/update.js +4 -1
- package/dist/server-fns/admin-activity/get.d.ts +3 -1
- package/dist/server-fns/admin-activity/get.js +4 -1
- package/dist/server-fns/admin-permissions/get-role-abilities.d.ts +3 -1
- package/dist/server-fns/admin-permissions/get-role-abilities.js +4 -1
- package/dist/server-fns/admin-permissions/index.d.ts +4 -4
- package/dist/server-fns/admin-permissions/list-registered.d.ts +3 -1
- package/dist/server-fns/admin-permissions/list-registered.js +4 -1
- package/dist/server-fns/admin-permissions/set-role-abilities.d.ts +3 -1
- package/dist/server-fns/admin-permissions/set-role-abilities.js +4 -1
- package/dist/server-fns/admin-permissions/who-has.d.ts +3 -1
- package/dist/server-fns/admin-permissions/who-has.js +4 -1
- package/dist/server-fns/admin-roles/create.d.ts +3 -1
- package/dist/server-fns/admin-roles/create.js +4 -1
- package/dist/server-fns/admin-roles/delete.d.ts +3 -1
- package/dist/server-fns/admin-roles/delete.js +4 -1
- package/dist/server-fns/admin-roles/get.d.ts +3 -1
- package/dist/server-fns/admin-roles/get.js +4 -1
- package/dist/server-fns/admin-roles/index.d.ts +6 -6
- package/dist/server-fns/admin-roles/list.d.ts +3 -1
- package/dist/server-fns/admin-roles/list.js +4 -1
- package/dist/server-fns/admin-roles/reorder.d.ts +3 -1
- package/dist/server-fns/admin-roles/reorder.js +4 -1
- package/dist/server-fns/admin-roles/update.d.ts +3 -1
- package/dist/server-fns/admin-roles/update.js +4 -1
- package/dist/server-fns/admin-users/create.d.ts +3 -1
- package/dist/server-fns/admin-users/create.js +4 -1
- package/dist/server-fns/admin-users/delete.d.ts +3 -1
- package/dist/server-fns/admin-users/delete.js +4 -1
- package/dist/server-fns/admin-users/disable.d.ts +3 -1
- package/dist/server-fns/admin-users/disable.js +4 -1
- package/dist/server-fns/admin-users/enable.d.ts +3 -1
- package/dist/server-fns/admin-users/enable.js +4 -1
- package/dist/server-fns/admin-users/get-user-roles.d.ts +3 -1
- package/dist/server-fns/admin-users/get-user-roles.js +4 -1
- package/dist/server-fns/admin-users/get.d.ts +3 -1
- package/dist/server-fns/admin-users/get.js +4 -1
- package/dist/server-fns/admin-users/index.d.ts +10 -10
- package/dist/server-fns/admin-users/list.d.ts +3 -1
- package/dist/server-fns/admin-users/list.js +4 -1
- package/dist/server-fns/admin-users/set-password.d.ts +3 -1
- package/dist/server-fns/admin-users/set-password.js +4 -1
- package/dist/server-fns/admin-users/set-user-roles.d.ts +3 -1
- package/dist/server-fns/admin-users/set-user-roles.js +4 -1
- package/dist/server-fns/admin-users/update.d.ts +3 -1
- package/dist/server-fns/admin-users/update.js +4 -1
- package/dist/server-fns/ai/execute.d.ts +3 -1
- package/dist/server-fns/ai/execute.js +4 -1
- package/dist/server-fns/ai/index.d.ts +1 -1
- package/dist/server-fns/analytics/maintenance.d.ts +6 -2
- package/dist/server-fns/analytics/maintenance.js +7 -2
- package/dist/server-fns/analytics/queries.d.ts +18 -6
- package/dist/server-fns/analytics/queries.js +19 -6
- package/dist/server-fns/auth/current-user.d.ts +1 -0
- package/dist/server-fns/auth/current-user.js +5 -2
- package/dist/server-fns/auth/renew.d.ts +13 -0
- package/dist/server-fns/auth/renew.js +45 -0
- package/dist/server-fns/auth/sign-in.d.ts +2 -1
- package/dist/server-fns/auth/sign-in.js +88 -17
- package/dist/server-fns/auth/sign-out.d.ts +3 -1
- package/dist/server-fns/auth/sign-out.js +38 -6
- package/dist/server-fns/collections/audit.d.ts +3 -1
- package/dist/server-fns/collections/audit.js +4 -1
- package/dist/server-fns/collections/copy-to-locale.d.ts +10 -1
- package/dist/server-fns/collections/copy-to-locale.js +5 -2
- package/dist/server-fns/collections/create.d.ts +10 -1
- package/dist/server-fns/collections/create.js +5 -2
- package/dist/server-fns/collections/delete-locale.d.ts +10 -1
- package/dist/server-fns/collections/delete-locale.js +5 -2
- package/dist/server-fns/collections/delete.d.ts +10 -1
- package/dist/server-fns/collections/delete.js +5 -2
- package/dist/server-fns/collections/duplicate.d.ts +10 -1
- package/dist/server-fns/collections/duplicate.js +5 -2
- package/dist/server-fns/collections/get.d.ts +7 -0
- package/dist/server-fns/collections/get.js +8 -3
- package/dist/server-fns/collections/history.d.ts +3 -1
- package/dist/server-fns/collections/history.js +4 -1
- package/dist/server-fns/collections/index.d.ts +21 -21
- package/dist/server-fns/collections/list.d.ts +10 -1
- package/dist/server-fns/collections/list.js +5 -2
- package/dist/server-fns/collections/reindex.d.ts +3 -1
- package/dist/server-fns/collections/reindex.js +4 -1
- package/dist/server-fns/collections/reorder.d.ts +10 -1
- package/dist/server-fns/collections/reorder.js +5 -2
- package/dist/server-fns/collections/restore-version.d.ts +10 -1
- package/dist/server-fns/collections/restore-version.js +5 -2
- package/dist/server-fns/collections/scheduled-publication.d.ts +22 -5
- package/dist/server-fns/collections/scheduled-publication.js +17 -6
- package/dist/server-fns/collections/set-list-view-preference.d.ts +3 -1
- package/dist/server-fns/collections/set-list-view-preference.js +4 -1
- package/dist/server-fns/collections/stats.js +4 -1
- package/dist/server-fns/collections/status.d.ts +13 -2
- package/dist/server-fns/collections/status.js +8 -3
- package/dist/server-fns/collections/tree.d.ts +22 -5
- package/dist/server-fns/collections/tree.js +17 -6
- package/dist/server-fns/collections/update.d.ts +13 -2
- package/dist/server-fns/collections/update.js +8 -3
- package/dist/server-fns/collections/upload.d.ts +10 -1
- package/dist/server-fns/collections/upload.js +4 -1
- package/dist/server-fns/i18n/set-locale.d.ts +3 -1
- package/dist/server-fns/i18n/set-locale.js +18 -3
- package/dist/server-fns/preview/disable.d.ts +3 -1
- package/dist/server-fns/preview/disable.js +4 -1
- package/dist/server-fns/preview/enable.d.ts +3 -1
- package/dist/server-fns/preview/enable.js +4 -1
- package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +10 -1
- package/dist/server-fns/singletons/cancel-scheduled-publish.js +5 -2
- package/dist/server-fns/singletons/change-status.d.ts +10 -1
- package/dist/server-fns/singletons/change-status.js +5 -2
- package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +10 -1
- package/dist/server-fns/singletons/confirm-scheduled-publish.js +5 -2
- package/dist/server-fns/singletons/copy-to-locale.d.ts +10 -1
- package/dist/server-fns/singletons/copy-to-locale.js +5 -2
- package/dist/server-fns/singletons/find-by-version.d.ts +3 -1
- package/dist/server-fns/singletons/find-by-version.js +4 -1
- package/dist/server-fns/singletons/get-scheduled-publish.d.ts +3 -1
- package/dist/server-fns/singletons/get-scheduled-publish.js +4 -1
- package/dist/server-fns/singletons/get.d.ts +3 -1
- package/dist/server-fns/singletons/get.js +4 -1
- package/dist/server-fns/singletons/history.d.ts +3 -1
- package/dist/server-fns/singletons/history.js +4 -1
- package/dist/server-fns/singletons/restore-version.d.ts +10 -1
- package/dist/server-fns/singletons/restore-version.js +5 -2
- package/dist/server-fns/singletons/schedule-publish.d.ts +10 -1
- package/dist/server-fns/singletons/schedule-publish.js +5 -2
- package/dist/server-fns/singletons/unpublish.d.ts +10 -1
- package/dist/server-fns/singletons/unpublish.js +5 -2
- package/dist/server-fns/singletons/update.d.ts +10 -1
- package/dist/server-fns/singletons/update.js +5 -2
- package/package.json +19 -19
- package/src/admin-shell/chrome/admin-app-bar.tsx +11 -6
- package/src/admin-shell/chrome/optional-i18n-provider.tsx +61 -0
- package/src/admin-shell/chrome/session-change-boundary.module.css +112 -0
- package/src/admin-shell/chrome/session-change-boundary.test.tsx +91 -0
- package/src/admin-shell/chrome/session-change-boundary.tsx +155 -0
- package/src/admin-shell/chrome/sign-in-page.tsx +68 -3
- package/src/integrations/byline-admin-services.ts +7 -1
- package/src/integrations/client-ip.test.node.ts +45 -0
- package/src/integrations/client-ip.ts +54 -0
- package/src/integrations/framework-fetch-shape.test.node.ts +56 -0
- package/src/integrations/session-client.test.node.ts +121 -0
- package/src/integrations/session-coordination.test.node.ts +77 -0
- package/src/integrations/session-coordination.ts +96 -0
- package/src/integrations/session-middleware.test.node.ts +87 -0
- package/src/integrations/session-middleware.ts +107 -0
- package/src/integrations/session-request-kind.ts +18 -0
- package/src/integrations/session-request-middleware.ts +17 -0
- package/src/integrations/sign-in-body.test.node.ts +59 -0
- package/src/integrations/sign-in-body.ts +50 -0
- package/src/integrations/sign-in-middleware.ts +24 -0
- package/src/routes/create-admin-layout-route.tsx +48 -30
- package/src/routes/create-sign-in-route.tsx +10 -2
- package/src/server-fns/admin-account/change-password.test.node.ts +69 -0
- package/src/server-fns/admin-account/change-password.ts +13 -2
- package/src/server-fns/admin-account/get.ts +2 -0
- package/src/server-fns/admin-account/update.ts +2 -0
- package/src/server-fns/admin-activity/get.ts +2 -0
- package/src/server-fns/admin-permissions/get-role-abilities.ts +2 -0
- package/src/server-fns/admin-permissions/list-registered.ts +5 -4
- package/src/server-fns/admin-permissions/set-role-abilities.ts +2 -0
- package/src/server-fns/admin-permissions/who-has.ts +2 -0
- package/src/server-fns/admin-roles/create.ts +2 -0
- package/src/server-fns/admin-roles/delete.ts +2 -0
- package/src/server-fns/admin-roles/get.ts +2 -0
- package/src/server-fns/admin-roles/list.ts +5 -4
- package/src/server-fns/admin-roles/reorder.ts +2 -0
- package/src/server-fns/admin-roles/update.ts +2 -0
- package/src/server-fns/admin-users/create.ts +2 -0
- package/src/server-fns/admin-users/delete.ts +2 -0
- package/src/server-fns/admin-users/disable.ts +2 -0
- package/src/server-fns/admin-users/enable.ts +2 -0
- package/src/server-fns/admin-users/get-user-roles.ts +2 -0
- package/src/server-fns/admin-users/get.ts +2 -0
- package/src/server-fns/admin-users/list.ts +2 -0
- package/src/server-fns/admin-users/set-password.ts +2 -0
- package/src/server-fns/admin-users/set-user-roles.ts +2 -0
- package/src/server-fns/admin-users/update.ts +2 -0
- package/src/server-fns/ai/execute.ts +3 -0
- package/src/server-fns/analytics/maintenance.ts +4 -0
- package/src/server-fns/analytics/queries.ts +14 -8
- package/src/server-fns/auth/current-user.ts +7 -3
- package/src/server-fns/auth/renew.test.node.ts +85 -0
- package/src/server-fns/auth/renew.ts +54 -0
- package/src/server-fns/auth/sign-in.test.node.ts +114 -0
- package/src/server-fns/auth/sign-in.ts +80 -27
- package/src/server-fns/auth/sign-out.test.node.ts +85 -0
- package/src/server-fns/auth/sign-out.ts +44 -19
- package/src/server-fns/collections/audit.ts +2 -0
- package/src/server-fns/collections/copy-to-locale.ts +3 -1
- package/src/server-fns/collections/create.ts +3 -1
- package/src/server-fns/collections/delete-locale.ts +3 -1
- package/src/server-fns/collections/delete.ts +3 -1
- package/src/server-fns/collections/duplicate.ts +3 -1
- package/src/server-fns/collections/get.ts +4 -1
- package/src/server-fns/collections/history.ts +2 -0
- package/src/server-fns/collections/list.ts +3 -1
- package/src/server-fns/collections/reindex.ts +2 -0
- package/src/server-fns/collections/reorder.ts +3 -1
- package/src/server-fns/collections/restore-version.ts +3 -1
- package/src/server-fns/collections/revision-input.test.node.ts +14 -0
- package/src/server-fns/collections/scheduled-publication.ts +14 -7
- package/src/server-fns/collections/set-list-view-preference.ts +2 -0
- package/src/server-fns/collections/stats.ts +2 -0
- package/src/server-fns/collections/status.ts +4 -1
- package/src/server-fns/collections/tree.ts +7 -1
- package/src/server-fns/collections/update.ts +4 -1
- package/src/server-fns/collections/upload-singleton.test.node.ts +8 -1
- package/src/server-fns/collections/upload.ts +10 -0
- package/src/server-fns/i18n/set-locale.ts +19 -3
- package/src/server-fns/preview/disable.ts +8 -4
- package/src/server-fns/preview/enable.ts +9 -5
- package/src/server-fns/singletons/cancel-scheduled-publish.ts +3 -1
- package/src/server-fns/singletons/change-status.ts +3 -1
- package/src/server-fns/singletons/confirm-scheduled-publish.ts +3 -1
- package/src/server-fns/singletons/copy-to-locale.ts +3 -1
- package/src/server-fns/singletons/find-by-version.ts +2 -0
- package/src/server-fns/singletons/get-scheduled-publish.ts +2 -0
- package/src/server-fns/singletons/get.ts +2 -0
- package/src/server-fns/singletons/history.ts +2 -0
- package/src/server-fns/singletons/restore-version.ts +3 -1
- package/src/server-fns/singletons/schedule-publish.ts +3 -1
- package/src/server-fns/singletons/singletons.test.node.ts +6 -0
- package/src/server-fns/singletons/unpublish.ts +3 -1
- package/src/server-fns/singletons/update.ts +3 -1
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
3
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
@@ -22,6 +20,8 @@ import { Branding } from './branding.js'
|
|
|
22
20
|
import { Breadcrumbs } from './breadcrumbs/breadcrumbs.js'
|
|
23
21
|
import { useBreadcrumbs } from './breadcrumbs/breadcrumbs-provider.js'
|
|
24
22
|
|
|
23
|
+
;('use client')
|
|
24
|
+
|
|
25
25
|
interface AdminAppBarProps {
|
|
26
26
|
user: CurrentAdminUser
|
|
27
27
|
}
|
|
@@ -37,16 +37,18 @@ export function AdminAppBar({ user }: AdminAppBarProps) {
|
|
|
37
37
|
const { breadCrumbSettings } = useBreadcrumbs()
|
|
38
38
|
const { t } = useTranslation('byline-admin')
|
|
39
39
|
const [signingOut, setSigningOut] = useState(false)
|
|
40
|
+
const [signOutError, setSignOutError] = useState(false)
|
|
40
41
|
|
|
41
42
|
async function handleSignOut() {
|
|
42
43
|
if (signingOut) return
|
|
43
44
|
setSigningOut(true)
|
|
45
|
+
setSignOutError(false)
|
|
44
46
|
try {
|
|
45
47
|
await adminSignOut()
|
|
46
|
-
} catch
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
} catch {
|
|
49
|
+
setSignOutError(true)
|
|
50
|
+
setSigningOut(false)
|
|
51
|
+
return
|
|
50
52
|
}
|
|
51
53
|
// Full-page navigation so the admin guard re-runs with cleared cookies.
|
|
52
54
|
window.location.assign(getSignInRoutePath())
|
|
@@ -71,6 +73,9 @@ export function AdminAppBar({ user }: AdminAppBarProps) {
|
|
|
71
73
|
{displayNameFor(user)}
|
|
72
74
|
</span>
|
|
73
75
|
</span>
|
|
76
|
+
{signOutError && (
|
|
77
|
+
<span role="alert">Sign-out could not be confirmed. Please try again.</span>
|
|
78
|
+
)}
|
|
74
79
|
<Button size="xs" intent="secondary" onClick={handleSignOut} disabled={signingOut}>
|
|
75
80
|
{signingOut ? t('common.actions.signingOut') : t('common.actions.signOut')}
|
|
76
81
|
</Button>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ReactNode, useContext } from 'react'
|
|
10
|
+
|
|
11
|
+
import { getAdminConfig } from '@byline/core'
|
|
12
|
+
import type { TranslationBundle } from '@byline/i18n'
|
|
13
|
+
import { I18nContext, I18nProvider } from '@byline/i18n/react'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Guarantees a translation context for chrome that can render *outside*
|
|
17
|
+
* the admin layout's `<I18nProvider>`.
|
|
18
|
+
*
|
|
19
|
+
* When a provider is already above us it is reused as-is, preserving the
|
|
20
|
+
* user's active locale. Only when none is present does this self-mount.
|
|
21
|
+
*
|
|
22
|
+
* Resilient by design: components using this are the ones shown when
|
|
23
|
+
* something has already gone wrong, so throwing here would mask the real
|
|
24
|
+
* condition. If the admin config can't be read it falls back to an
|
|
25
|
+
* English-default provider with an empty bundle — with a provider always
|
|
26
|
+
* present, `useTranslation` never throws, and a missing key renders as the
|
|
27
|
+
* raw key rather than crashing (see `I18nProvider`'s `onMissing`).
|
|
28
|
+
*
|
|
29
|
+
* `route-error.tsx` carries its own equivalent copy of this logic; it was
|
|
30
|
+
* left in place deliberately rather than refactored alongside an unrelated
|
|
31
|
+
* change, and can adopt this component whenever that file is next touched.
|
|
32
|
+
*/
|
|
33
|
+
export function OptionalI18nProvider({ children }: { children: ReactNode }) {
|
|
34
|
+
// Already inside a provider — reuse it (keeps the active locale) and skip
|
|
35
|
+
// the self-mount entirely.
|
|
36
|
+
if (useContext(I18nContext) != null) {
|
|
37
|
+
return <>{children}</>
|
|
38
|
+
}
|
|
39
|
+
let bundle: TranslationBundle = {}
|
|
40
|
+
let locale = 'en'
|
|
41
|
+
try {
|
|
42
|
+
const { i18n } = getAdminConfig()
|
|
43
|
+
bundle = i18n.translations ?? {}
|
|
44
|
+
// No active provider, so render in the default locale — recovering the
|
|
45
|
+
// per-user active locale isn't worth another lookup that could itself throw.
|
|
46
|
+
locale = i18n.admin.defaultLocale
|
|
47
|
+
} catch {
|
|
48
|
+
// Config unavailable (very early boot / broken provider chain) — fall
|
|
49
|
+
// through with the English defaults above.
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
<I18nProvider
|
|
53
|
+
bundle={bundle}
|
|
54
|
+
activeLocale={locale}
|
|
55
|
+
defaultLocale={locale}
|
|
56
|
+
localeDefinitions={[]}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</I18nProvider>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionChangeBoundary — viewport-centred interstitial shown when the
|
|
3
|
+
* active login identity no longer matches the one this page was rendered
|
|
4
|
+
* for. Rare by design: it only appears on an account mismatch.
|
|
5
|
+
*
|
|
6
|
+
* Override handles:
|
|
7
|
+
* .byline-session-change — outer <main>, centres the card in the viewport
|
|
8
|
+
* .byline-session-change-card — the bordered card itself
|
|
9
|
+
* .byline-session-change-title — heading
|
|
10
|
+
* .byline-session-change-body — explanatory paragraph
|
|
11
|
+
* .byline-session-change-account — "Active account: …" line
|
|
12
|
+
* .byline-session-change-error — error paragraph (role="alert")
|
|
13
|
+
* .byline-session-change-actions — button row
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.main,
|
|
17
|
+
:global(.byline-session-change) {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
min-height: 100dvh;
|
|
22
|
+
padding: 1.5rem;
|
|
23
|
+
/* Bias the card just above the geometric centre — a dialog sitting dead
|
|
24
|
+
centre optically reads as slightly low. The extra bottom padding lifts it
|
|
25
|
+
by half its own value, and being proportional it degrades gracefully on
|
|
26
|
+
short viewports instead of clipping the card off the top. */
|
|
27
|
+
padding-bottom: calc(1.5rem + 12vh);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.card,
|
|
31
|
+
:global(.byline-session-change-card) {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 0.75rem;
|
|
35
|
+
width: 100%;
|
|
36
|
+
max-width: 30rem;
|
|
37
|
+
padding: 1.5rem;
|
|
38
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
|
|
39
|
+
border-radius: var(--border-radius-md);
|
|
40
|
+
background-color: var(--canvas-25);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:is([data-theme="dark"], :global(.dark)) .card,
|
|
44
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-session-change-card) {
|
|
45
|
+
border-color: var(--gray-700);
|
|
46
|
+
background-color: var(--canvas-800);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.title,
|
|
50
|
+
:global(.byline-session-change-title) {
|
|
51
|
+
margin: 0;
|
|
52
|
+
font-size: var(--font-size-xl);
|
|
53
|
+
font-weight: var(--font-weight-semibold);
|
|
54
|
+
line-height: var(--line-height-snug);
|
|
55
|
+
letter-spacing: var(--letter-spacing-tight);
|
|
56
|
+
color: var(--gray-900);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:is([data-theme="dark"], :global(.dark)) .title,
|
|
60
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-session-change-title) {
|
|
61
|
+
color: var(--gray-100);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.body,
|
|
65
|
+
:global(.byline-session-change-body) {
|
|
66
|
+
margin: 0;
|
|
67
|
+
font-size: var(--font-size-sm);
|
|
68
|
+
line-height: var(--line-height-relaxed);
|
|
69
|
+
color: var(--gray-700);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:is([data-theme="dark"], :global(.dark)) .body,
|
|
73
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-session-change-body) {
|
|
74
|
+
color: var(--gray-400);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.account,
|
|
78
|
+
:global(.byline-session-change-account) {
|
|
79
|
+
margin: 0;
|
|
80
|
+
font-size: var(--font-size-sm);
|
|
81
|
+
font-weight: var(--font-weight-medium);
|
|
82
|
+
line-height: var(--line-height-normal);
|
|
83
|
+
color: var(--gray-900);
|
|
84
|
+
overflow-wrap: anywhere;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
:is([data-theme="dark"], :global(.dark)) .account,
|
|
88
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-session-change-account) {
|
|
89
|
+
color: var(--gray-100);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.error,
|
|
93
|
+
:global(.byline-session-change-error) {
|
|
94
|
+
margin: 0;
|
|
95
|
+
font-size: var(--font-size-sm);
|
|
96
|
+
line-height: var(--line-height-relaxed);
|
|
97
|
+
color: var(--red-600);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:is([data-theme="dark"], :global(.dark)) .error,
|
|
101
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-session-change-error) {
|
|
102
|
+
color: var(--red-400);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.actions,
|
|
106
|
+
:global(.byline-session-change-actions) {
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-wrap: wrap;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 0.75rem;
|
|
111
|
+
margin-top: 0.25rem;
|
|
112
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { act } from 'react'
|
|
10
|
+
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
13
|
+
|
|
14
|
+
const mocks = vi.hoisted(() => ({ current: vi.fn() }))
|
|
15
|
+
vi.mock('../../server-fns/auth/current-user.js', () => ({ getCurrentAdminUser: mocks.current }))
|
|
16
|
+
vi.mock('../../routes/sign-in-path.js', () => ({ getSignInRoutePath: () => '/sign-in' }))
|
|
17
|
+
|
|
18
|
+
import { adminTranslations } from '@byline/i18n/admin'
|
|
19
|
+
import { I18nProvider } from '@byline/i18n/react'
|
|
20
|
+
|
|
21
|
+
import { acceptSession, expectedSession } from '../../integrations/session-coordination.js'
|
|
22
|
+
import { SessionChangeBoundary } from './session-change-boundary.js'
|
|
23
|
+
|
|
24
|
+
const user = (sessionId: string, email: string) => ({
|
|
25
|
+
sessionId,
|
|
26
|
+
email,
|
|
27
|
+
id: email,
|
|
28
|
+
given_name: null,
|
|
29
|
+
family_name: null,
|
|
30
|
+
is_super_admin: false,
|
|
31
|
+
abilities: [],
|
|
32
|
+
})
|
|
33
|
+
let root: Root
|
|
34
|
+
let container: HTMLDivElement
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks()
|
|
37
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
38
|
+
vi.stubGlobal('BroadcastChannel', undefined)
|
|
39
|
+
window.sessionStorage.clear()
|
|
40
|
+
acceptSession('expected-B')
|
|
41
|
+
container = document.createElement('div')
|
|
42
|
+
document.body.appendChild(container)
|
|
43
|
+
root = createRoot(container)
|
|
44
|
+
})
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
act(() => root.unmount())
|
|
47
|
+
container.remove()
|
|
48
|
+
vi.unstubAllGlobals()
|
|
49
|
+
})
|
|
50
|
+
// Mounted inside a real provider so the assertions below read the shipped
|
|
51
|
+
// English strings, and so the boundary's `OptionalI18nProvider` exercises its
|
|
52
|
+
// reuse path (an existing context is preserved rather than self-mounted).
|
|
53
|
+
async function render() {
|
|
54
|
+
await act(async () => {
|
|
55
|
+
root.render(
|
|
56
|
+
<I18nProvider
|
|
57
|
+
bundle={adminTranslations({ locales: ['en'] })}
|
|
58
|
+
activeLocale="en"
|
|
59
|
+
defaultLocale="en"
|
|
60
|
+
localeDefinitions={[{ code: 'en', nativeName: 'English' }]}
|
|
61
|
+
>
|
|
62
|
+
<SessionChangeBoundary user={user('late-A', 'x@example.test')}>
|
|
63
|
+
<button type="button">Publish</button>
|
|
64
|
+
</SessionChangeBoundary>
|
|
65
|
+
</I18nProvider>
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
describe('session-change acknowledgement', () => {
|
|
70
|
+
it('blocks business UI on the first post-sign-in render when cookies select a different login', async () => {
|
|
71
|
+
mocks.current.mockResolvedValue(user('late-A', 'x@example.test'))
|
|
72
|
+
await render()
|
|
73
|
+
expect(container.textContent).not.toContain('Publish')
|
|
74
|
+
expect(container.querySelector('[role="alertdialog"]')).not.toBeNull()
|
|
75
|
+
expect(container.textContent).toContain('Active account: x@example.test')
|
|
76
|
+
expect(expectedSession()).toBe('expected-B')
|
|
77
|
+
})
|
|
78
|
+
it('requires renewed acknowledgement when the server account changes again', async () => {
|
|
79
|
+
mocks.current.mockResolvedValue(user('late-A', 'x@example.test'))
|
|
80
|
+
await render()
|
|
81
|
+
mocks.current.mockResolvedValue(user('new-C', 'z@example.test'))
|
|
82
|
+
await act(async () => {
|
|
83
|
+
container.querySelector('button')?.click()
|
|
84
|
+
})
|
|
85
|
+
expect(container.textContent).toContain(
|
|
86
|
+
'The active session changed again. Review the account before continuing.'
|
|
87
|
+
)
|
|
88
|
+
expect(expectedSession()).toBe('expected-B')
|
|
89
|
+
expect(container.textContent).not.toContain('Publish')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ReactNode, useEffect, useState, useSyncExternalStore } from 'react'
|
|
10
|
+
|
|
11
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
12
|
+
import { Button } from '@byline/ui/react'
|
|
13
|
+
import cx from 'clsx'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
acceptSession,
|
|
17
|
+
expectedSession,
|
|
18
|
+
flagSessionChanged,
|
|
19
|
+
observeSession,
|
|
20
|
+
sessionEpoch,
|
|
21
|
+
sessionIsChanged,
|
|
22
|
+
startSessionNotifications,
|
|
23
|
+
subscribeSessionChange,
|
|
24
|
+
} from '../../integrations/session-coordination.js'
|
|
25
|
+
import { getSignInRoutePath } from '../../routes/sign-in-path.js'
|
|
26
|
+
import { type CurrentAdminUser, getCurrentAdminUser } from '../../server-fns/auth/current-user.js'
|
|
27
|
+
import { OptionalI18nProvider } from './optional-i18n-provider.js'
|
|
28
|
+
import styles from './session-change-boundary.module.css'
|
|
29
|
+
|
|
30
|
+
/** Blocks the page until the user explicitly acknowledges freshly verified login identity. */
|
|
31
|
+
export function SessionChangeBoundary({
|
|
32
|
+
user,
|
|
33
|
+
children,
|
|
34
|
+
}: {
|
|
35
|
+
user?: CurrentAdminUser
|
|
36
|
+
children: ReactNode
|
|
37
|
+
}) {
|
|
38
|
+
const epoch = useSyncExternalStore(subscribeSessionChange, sessionEpoch, () => 0)
|
|
39
|
+
const changed = sessionIsChanged()
|
|
40
|
+
const [current, setCurrent] = useState(user)
|
|
41
|
+
const [error, setError] = useState<string>()
|
|
42
|
+
const [busy, setBusy] = useState(false)
|
|
43
|
+
const mismatch =
|
|
44
|
+
typeof window !== 'undefined' &&
|
|
45
|
+
!!user &&
|
|
46
|
+
!!expectedSession() &&
|
|
47
|
+
expectedSession() !== user.sessionId
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
startSessionNotifications()
|
|
50
|
+
if (user) {
|
|
51
|
+
try {
|
|
52
|
+
observeSession(user.sessionId)
|
|
53
|
+
} catch {
|
|
54
|
+
/* Interstitial owns this mismatch. */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, [user])
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!changed && !mismatch) return
|
|
60
|
+
void getCurrentAdminUser()
|
|
61
|
+
.then((verified) => {
|
|
62
|
+
if (sessionEpoch() !== epoch) return
|
|
63
|
+
setCurrent(verified)
|
|
64
|
+
setError(undefined)
|
|
65
|
+
})
|
|
66
|
+
.catch(() => setError('sessionChange.errors.unavailable'))
|
|
67
|
+
}, [changed, mismatch, epoch])
|
|
68
|
+
if (!changed && !mismatch) return children
|
|
69
|
+
async function acknowledge() {
|
|
70
|
+
setBusy(true)
|
|
71
|
+
try {
|
|
72
|
+
const verified = await getCurrentAdminUser()
|
|
73
|
+
// Require a second acknowledgement if the identity changed while this interstitial was open.
|
|
74
|
+
if (!current || current.sessionId !== verified.sessionId) {
|
|
75
|
+
setCurrent(verified)
|
|
76
|
+
setError('sessionChange.errors.changedAgain')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
acceptSession(verified.sessionId)
|
|
80
|
+
window.location.reload() // Discard queued edits and old page state; never replay them.
|
|
81
|
+
} catch {
|
|
82
|
+
flagSessionChanged()
|
|
83
|
+
setError('sessionChange.errors.unverifiable')
|
|
84
|
+
} finally {
|
|
85
|
+
setBusy(false)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return (
|
|
89
|
+
<OptionalI18nProvider>
|
|
90
|
+
<SessionChangeInterstitial
|
|
91
|
+
current={current}
|
|
92
|
+
errorKey={error}
|
|
93
|
+
busy={busy}
|
|
94
|
+
onAcknowledge={acknowledge}
|
|
95
|
+
/>
|
|
96
|
+
</OptionalI18nProvider>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Presentation half. Split out so the error state can be carried as a
|
|
102
|
+
* translation *key* through the logic above and resolved here, inside the
|
|
103
|
+
* provider — the boundary itself renders outside any `<I18nProvider>` at
|
|
104
|
+
* both of its mount sites.
|
|
105
|
+
*/
|
|
106
|
+
function SessionChangeInterstitial({
|
|
107
|
+
current,
|
|
108
|
+
errorKey,
|
|
109
|
+
busy,
|
|
110
|
+
onAcknowledge,
|
|
111
|
+
}: {
|
|
112
|
+
current?: CurrentAdminUser
|
|
113
|
+
errorKey?: string
|
|
114
|
+
busy: boolean
|
|
115
|
+
onAcknowledge: () => void
|
|
116
|
+
}) {
|
|
117
|
+
const { t } = useTranslation('byline-admin')
|
|
118
|
+
return (
|
|
119
|
+
<main
|
|
120
|
+
className={cx('byline-session-change', styles.main)}
|
|
121
|
+
role="alertdialog"
|
|
122
|
+
aria-modal="true"
|
|
123
|
+
aria-labelledby="session-change-title"
|
|
124
|
+
>
|
|
125
|
+
<div className={cx('byline-session-change-card', styles.card)}>
|
|
126
|
+
<h1 id="session-change-title" className={cx('byline-session-change-title', styles.title)}>
|
|
127
|
+
{t('sessionChange.title')}
|
|
128
|
+
</h1>
|
|
129
|
+
<p className={cx('byline-session-change-body', styles.body)}>{t('sessionChange.body')}</p>
|
|
130
|
+
{current && (
|
|
131
|
+
<p className={cx('byline-session-change-account', styles.account)}>
|
|
132
|
+
{t('sessionChange.activeAccount', { email: current.email })}
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
{errorKey && (
|
|
136
|
+
<p className={cx('byline-session-change-error', styles.error)} role="alert">
|
|
137
|
+
{t(errorKey)}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
<div className={cx('byline-session-change-actions', styles.actions)}>
|
|
141
|
+
<Button type="button" disabled={busy || !current} onClick={onAcknowledge}>
|
|
142
|
+
{t('sessionChange.continue')}
|
|
143
|
+
</Button>
|
|
144
|
+
<Button
|
|
145
|
+
variant="outlined"
|
|
146
|
+
intent="noeffect"
|
|
147
|
+
render={<a href={`${getSignInRoutePath()}?reauthenticate=1`} />}
|
|
148
|
+
>
|
|
149
|
+
{t('sessionChange.signInAgain')}
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</main>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { useEffect, useState } from 'react'
|
|
10
|
+
|
|
9
11
|
import { SignInForm } from '@byline/admin/auth/components/sign-in-form'
|
|
10
12
|
import { BylineAdminServicesProvider } from '@byline/admin/services'
|
|
11
13
|
import { getAdminConfig } from '@byline/core'
|
|
@@ -15,10 +17,20 @@ import cx from 'clsx'
|
|
|
15
17
|
|
|
16
18
|
import { buildLocaleDefinitions } from '../../i18n/locale-definitions.js'
|
|
17
19
|
import { bylineAdminServices } from '../../integrations/byline-admin-services.js'
|
|
20
|
+
import {
|
|
21
|
+
coordinateAuthAction,
|
|
22
|
+
expectedSession,
|
|
23
|
+
flagSessionChanged,
|
|
24
|
+
observeSession,
|
|
25
|
+
renewSingleFlight,
|
|
26
|
+
} from '../../integrations/session-coordination.js'
|
|
27
|
+
import { renewAdminSession } from '../../server-fns/auth/renew.js'
|
|
18
28
|
import { setAdminLocaleFn } from '../../server-fns/i18n/index.js'
|
|
29
|
+
import { SessionChangeBoundary } from './session-change-boundary.js'
|
|
19
30
|
import styles from './sign-in-page.module.css'
|
|
20
31
|
|
|
21
32
|
interface SignInPageProps {
|
|
33
|
+
reauthenticate?: boolean
|
|
22
34
|
redirectTo: string
|
|
23
35
|
activeLocale: LocaleCode
|
|
24
36
|
homeUrl?: string
|
|
@@ -44,14 +56,54 @@ interface SignInPageProps {
|
|
|
44
56
|
* Threads an optional host-owned `homeUrl` into `SignInForm` so the form's
|
|
45
57
|
* action row can render a plain "Home" link beside the submit button.
|
|
46
58
|
*/
|
|
47
|
-
export function SignInPage({
|
|
59
|
+
export function SignInPage({
|
|
60
|
+
redirectTo,
|
|
61
|
+
activeLocale,
|
|
62
|
+
homeUrl,
|
|
63
|
+
reauthenticate = false,
|
|
64
|
+
}: SignInPageProps) {
|
|
65
|
+
const [checking, setChecking] = useState(!reauthenticate)
|
|
66
|
+
const [bootstrapError, setBootstrapError] = useState(false)
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (reauthenticate) return
|
|
69
|
+
let active = true
|
|
70
|
+
void renewSingleFlight(() =>
|
|
71
|
+
coordinateAuthAction(async () => {
|
|
72
|
+
const result = await renewAdminSession({
|
|
73
|
+
data: { expectedSessionId: expectedSession() ?? undefined },
|
|
74
|
+
})
|
|
75
|
+
observeSession(result.sessionId)
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
.then(() => {
|
|
79
|
+
if (active) window.location.replace(redirectTo)
|
|
80
|
+
})
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
if (error?.code === 'ERR_SESSION_CHANGED') flagSessionChanged()
|
|
83
|
+
else if (
|
|
84
|
+
![
|
|
85
|
+
'ERR_UNAUTHENTICATED',
|
|
86
|
+
'ERR_INVALID_TOKEN',
|
|
87
|
+
'ERR_REVOKED_TOKEN',
|
|
88
|
+
'ERR_ACCOUNT_DISABLED',
|
|
89
|
+
].includes(error?.code)
|
|
90
|
+
)
|
|
91
|
+
setBootstrapError(true)
|
|
92
|
+
})
|
|
93
|
+
.finally(() => {
|
|
94
|
+
if (active) setChecking(false)
|
|
95
|
+
})
|
|
96
|
+
return () => {
|
|
97
|
+
active = false
|
|
98
|
+
}
|
|
99
|
+
}, [redirectTo, reauthenticate])
|
|
48
100
|
const { i18n } = getAdminConfig()
|
|
49
101
|
const localeDefinitions = buildLocaleDefinitions(i18n.admin.locales, i18n.admin.localeDefinitions)
|
|
50
102
|
const handleSetLocale = async (next: LocaleCode) => {
|
|
51
103
|
await setAdminLocaleFn({ data: { locale: next } })
|
|
52
104
|
window.location.reload()
|
|
53
105
|
}
|
|
54
|
-
|
|
106
|
+
const content = (
|
|
55
107
|
<I18nProvider
|
|
56
108
|
bundle={i18n.translations ?? {}}
|
|
57
109
|
activeLocale={activeLocale}
|
|
@@ -61,14 +113,27 @@ export function SignInPage({ redirectTo, activeLocale, homeUrl }: SignInPageProp
|
|
|
61
113
|
>
|
|
62
114
|
<BylineAdminServicesProvider services={bylineAdminServices}>
|
|
63
115
|
<main className={cx('byline-sign-in-page', styles.main)}>
|
|
116
|
+
<noscript>JavaScript is required to sign in or renew your admin session.</noscript>
|
|
64
117
|
<div className={cx('byline-sign-in-page-bar', styles.bar)}>
|
|
65
118
|
<LanguageMenu />
|
|
66
119
|
</div>
|
|
67
120
|
<div className={cx('byline-sign-in-page-inner', styles.inner)}>
|
|
68
|
-
|
|
121
|
+
{bootstrapError ? (
|
|
122
|
+
<div role="alert">
|
|
123
|
+
<p>Unable to check your session. Please try again.</p>
|
|
124
|
+
<button type="button" onClick={() => window.location.reload()}>
|
|
125
|
+
Try again
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
) : checking ? (
|
|
129
|
+
<p role="status">Checking your session…</p>
|
|
130
|
+
) : (
|
|
131
|
+
<SignInForm redirectTo={redirectTo} homeUrl={homeUrl} />
|
|
132
|
+
)}
|
|
69
133
|
</div>
|
|
70
134
|
</main>
|
|
71
135
|
</BylineAdminServicesProvider>
|
|
72
136
|
</I18nProvider>
|
|
73
137
|
)
|
|
138
|
+
return reauthenticate ? content : <SessionChangeBoundary>{content}</SessionChangeBoundary>
|
|
74
139
|
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { adminSignIn } from '../server-fns/auth/index.js'
|
|
37
37
|
import { getCollectionDocumentVersion as serverGetCollectionDocumentVersion } from '../server-fns/collections/get.js'
|
|
38
38
|
import { setAdminLocaleFn } from '../server-fns/i18n/index.js'
|
|
39
|
+
import { acceptSession, coordinateAuthAction, notifySessionAction } from './session-coordination.js'
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Diff helper adapter — the contract uses positional args; the underlying
|
|
@@ -62,7 +63,12 @@ const getCollectionDocumentVersion: BylineAdminServices['getCollectionDocumentVe
|
|
|
62
63
|
|
|
63
64
|
export const bylineAdminServices: BylineAdminServices = {
|
|
64
65
|
// Auth
|
|
65
|
-
adminSignIn
|
|
66
|
+
adminSignIn: async (args) => {
|
|
67
|
+
const result = await coordinateAuthAction(() => adminSignIn(args))
|
|
68
|
+
acceptSession(result.sessionId)
|
|
69
|
+
notifySessionAction()
|
|
70
|
+
return result
|
|
71
|
+
},
|
|
66
72
|
|
|
67
73
|
// Account self-service
|
|
68
74
|
updateAccount,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { createClientIpResolver, normalizeClientIp } from './client-ip.js'
|
|
12
|
+
|
|
13
|
+
describe('client IP trust boundary', () => {
|
|
14
|
+
it('ignores spoofed forwarding headers in direct mode', async () => {
|
|
15
|
+
const resolve = createClientIpResolver({ resolvePeerIp: () => '192.0.2.1' })
|
|
16
|
+
expect(
|
|
17
|
+
await resolve(
|
|
18
|
+
new Request('https://app.test', {
|
|
19
|
+
headers: { 'x-forwarded-for': '198.51.100.1', 'x-real-ip': '198.51.100.2' },
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
).toBe('192.0.2.1')
|
|
23
|
+
})
|
|
24
|
+
it('uses only the configured proxy header and fails closed on absent/invalid values', async () => {
|
|
25
|
+
const resolve = createClientIpResolver({
|
|
26
|
+
trustedProxyHeader: 'x-byline-client-ip',
|
|
27
|
+
resolvePeerIp: () => '127.0.0.1',
|
|
28
|
+
})
|
|
29
|
+
for (const value of ['', '192.0.2.1, 192.0.2.2', 'garbage']) {
|
|
30
|
+
expect(
|
|
31
|
+
await resolve(new Request('https://app.test', { headers: { 'x-byline-client-ip': value } }))
|
|
32
|
+
).toBeNull()
|
|
33
|
+
}
|
|
34
|
+
expect(
|
|
35
|
+
await resolve(
|
|
36
|
+
new Request('https://app.test', { headers: { 'x-byline-client-ip': '2001:0db8::1' } })
|
|
37
|
+
)
|
|
38
|
+
).toBe('2001:db8::1')
|
|
39
|
+
})
|
|
40
|
+
it('normalizes equivalent IP representations', () => {
|
|
41
|
+
expect(normalizeClientIp('::ffff:192.0.2.1')).toBe('192.0.2.1')
|
|
42
|
+
expect(normalizeClientIp('::ffff:c000:201')).toBe('192.0.2.1')
|
|
43
|
+
expect(normalizeClientIp('fe80::1%en0')).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
})
|