@byline/host-tanstack-start 5.1.2 → 5.1.4
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/sign-in-page.js +76 -34
- package/dist/admin-shell/chrome/sign-in-page.module.js +6 -1
- package/dist/admin-shell/chrome/sign-in-page.test.d.ts +8 -0
- package/dist/admin-shell/chrome/sign-in-page_module.css +49 -0
- package/dist/integrations/admin-session-recovery.d.ts +10 -0
- package/dist/integrations/admin-session-recovery.js +33 -0
- package/dist/integrations/admin-session-recovery.test.d.ts +8 -0
- package/dist/integrations/session-renewal.d.ts +8 -0
- package/dist/integrations/session-renewal.js +16 -0
- package/dist/integrations/session-renewal.test.d.ts +8 -0
- package/dist/routes/admin-layout-guard.d.ts +20 -0
- package/dist/routes/admin-layout-guard.js +40 -0
- package/dist/routes/create-admin-analytics-route.js +17 -5
- package/dist/routes/create-admin-layout-route.js +25 -35
- package/dist/server-fns/auth/current-user.d.ts +18 -2
- package/dist/server-fns/auth/current-user.js +32 -15
- package/dist/server-fns/auth/index.d.ts +1 -1
- package/dist/server-fns/auth/index.js +1 -1
- package/package.json +10 -10
- package/src/admin-shell/chrome/sign-in-page.module.css +59 -0
- package/src/admin-shell/chrome/sign-in-page.test.tsx +99 -0
- package/src/admin-shell/chrome/sign-in-page.tsx +81 -25
- package/src/integrations/admin-session-recovery.test.tsx +120 -0
- package/src/integrations/admin-session-recovery.tsx +72 -0
- package/src/integrations/session-renewal.test.tsx +72 -0
- package/src/integrations/session-renewal.ts +45 -0
- package/src/routes/admin-layout-guard.test.node.ts +136 -0
- package/src/routes/admin-layout-guard.ts +91 -0
- package/src/routes/create-admin-analytics-route.tsx +23 -5
- package/src/routes/create-admin-layout-route.tsx +33 -46
- package/src/server-fns/auth/current-user.test.node.ts +85 -0
- package/src/server-fns/auth/current-user.ts +49 -19
- package/src/server-fns/auth/index.ts +2 -0
|
@@ -3,12 +3,13 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { SignInForm } from "@byline/admin/auth/components/sign-in-form";
|
|
4
4
|
import { BylineAdminServicesProvider } from "@byline/admin/services";
|
|
5
5
|
import { getAdminConfig } from "@byline/core";
|
|
6
|
-
import { I18nProvider, LanguageMenu } from "@byline/i18n/react";
|
|
6
|
+
import { I18nProvider, LanguageMenu, useTranslation } from "@byline/i18n/react";
|
|
7
|
+
import { Alert, Button, Card, LoaderRing } from "@byline/ui/react";
|
|
7
8
|
import clsx from "clsx";
|
|
8
9
|
import { buildLocaleDefinitions } from "../../i18n/locale-definitions.js";
|
|
9
10
|
import { bylineAdminServices } from "../../integrations/byline-admin-services.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { flagSessionChanged } from "../../integrations/session-coordination.js";
|
|
12
|
+
import { renewExpectedSession } from "../../integrations/session-renewal.js";
|
|
12
13
|
import { setAdminLocaleFn } from "../../server-fns/i18n/index.js";
|
|
13
14
|
import { SessionChangeBoundary } from "./session-change-boundary.js";
|
|
14
15
|
import sign_in_page_module from "./sign-in-page.module.js";
|
|
@@ -18,14 +19,7 @@ function SignInPage({ redirectTo, activeLocale, homeUrl, reauthenticate = false
|
|
|
18
19
|
useEffect(()=>{
|
|
19
20
|
if (reauthenticate) return;
|
|
20
21
|
let active = true;
|
|
21
|
-
|
|
22
|
-
const result = await renewAdminSession({
|
|
23
|
-
data: {
|
|
24
|
-
expectedSessionId: expectedSession() ?? void 0
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
observeSession(result.sessionId);
|
|
28
|
-
})).then(()=>{
|
|
22
|
+
renewExpectedSession().then(()=>{
|
|
29
23
|
if (active) window.location.replace(redirectTo);
|
|
30
24
|
}).catch((error)=>{
|
|
31
25
|
if (error?.code === 'ERR_SESSION_CHANGED') flagSessionChanged();
|
|
@@ -66,34 +60,19 @@ function SignInPage({ redirectTo, activeLocale, homeUrl, reauthenticate = false
|
|
|
66
60
|
children: /*#__PURE__*/ jsxs("main", {
|
|
67
61
|
className: clsx('byline-sign-in-page', sign_in_page_module.main),
|
|
68
62
|
children: [
|
|
69
|
-
/*#__PURE__*/ jsx("noscript", {
|
|
70
|
-
children: "JavaScript is required to sign in or renew your admin session."
|
|
71
|
-
}),
|
|
72
63
|
/*#__PURE__*/ jsx("div", {
|
|
73
64
|
className: clsx('byline-sign-in-page-bar', sign_in_page_module.bar),
|
|
74
65
|
children: /*#__PURE__*/ jsx(LanguageMenu, {})
|
|
75
66
|
}),
|
|
76
|
-
/*#__PURE__*/
|
|
67
|
+
/*#__PURE__*/ jsxs("div", {
|
|
77
68
|
className: clsx('byline-sign-in-page-inner', sign_in_page_module.inner),
|
|
78
|
-
children:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
type: "button",
|
|
86
|
-
onClick: ()=>window.location.reload(),
|
|
87
|
-
children: "Try again"
|
|
88
|
-
})
|
|
89
|
-
]
|
|
90
|
-
}) : checking ? /*#__PURE__*/ jsx("p", {
|
|
91
|
-
role: "status",
|
|
92
|
-
children: "Checking your session…"
|
|
93
|
-
}) : /*#__PURE__*/ jsx(SignInForm, {
|
|
94
|
-
redirectTo: redirectTo,
|
|
95
|
-
homeUrl: homeUrl
|
|
96
|
-
})
|
|
69
|
+
children: [
|
|
70
|
+
/*#__PURE__*/ jsx(NoScriptNotice, {}),
|
|
71
|
+
bootstrapError ? /*#__PURE__*/ jsx(SessionCheckError, {}) : checking ? /*#__PURE__*/ jsx(SessionCheckIndicator, {}) : /*#__PURE__*/ jsx(SignInForm, {
|
|
72
|
+
redirectTo: redirectTo,
|
|
73
|
+
homeUrl: homeUrl
|
|
74
|
+
})
|
|
75
|
+
]
|
|
97
76
|
})
|
|
98
77
|
]
|
|
99
78
|
})
|
|
@@ -103,4 +82,67 @@ function SignInPage({ redirectTo, activeLocale, homeUrl, reauthenticate = false
|
|
|
103
82
|
children: content
|
|
104
83
|
});
|
|
105
84
|
}
|
|
85
|
+
function SessionCheckIndicator() {
|
|
86
|
+
const { t } = useTranslation('byline-admin');
|
|
87
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
88
|
+
role: "status",
|
|
89
|
+
"aria-live": "polite",
|
|
90
|
+
className: clsx('byline-sign-in-page-checking', sign_in_page_module.checking),
|
|
91
|
+
children: [
|
|
92
|
+
/*#__PURE__*/ jsx(LoaderRing, {
|
|
93
|
+
size: 32,
|
|
94
|
+
"aria-hidden": "true"
|
|
95
|
+
}),
|
|
96
|
+
/*#__PURE__*/ jsx("span", {
|
|
97
|
+
className: sign_in_page_module.srOnly,
|
|
98
|
+
children: t('common.loading')
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function NoScriptNotice() {
|
|
104
|
+
const { t } = useTranslation('byline-admin');
|
|
105
|
+
return /*#__PURE__*/ jsx("noscript", {
|
|
106
|
+
children: /*#__PURE__*/ jsx(Alert, {
|
|
107
|
+
intent: "warning",
|
|
108
|
+
close: false,
|
|
109
|
+
className: clsx('byline-sign-in-page-alert', sign_in_page_module.alert),
|
|
110
|
+
children: t("auth.signIn.noscript")
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function SessionCheckError() {
|
|
115
|
+
const { t } = useTranslation('byline-admin');
|
|
116
|
+
return /*#__PURE__*/ jsxs(Card, {
|
|
117
|
+
role: "alert",
|
|
118
|
+
className: clsx('byline-sign-in-page-card', sign_in_page_module.card),
|
|
119
|
+
children: [
|
|
120
|
+
/*#__PURE__*/ jsxs(Card.Header, {
|
|
121
|
+
children: [
|
|
122
|
+
/*#__PURE__*/ jsx(Card.Title, {
|
|
123
|
+
children: /*#__PURE__*/ jsx("h2", {
|
|
124
|
+
children: t('auth.signIn.title')
|
|
125
|
+
})
|
|
126
|
+
}),
|
|
127
|
+
/*#__PURE__*/ jsx(Alert, {
|
|
128
|
+
intent: "danger",
|
|
129
|
+
close: false,
|
|
130
|
+
className: clsx('byline-sign-in-page-alert', sign_in_page_module.alert),
|
|
131
|
+
children: t('auth.signIn.errors.bootstrap')
|
|
132
|
+
})
|
|
133
|
+
]
|
|
134
|
+
}),
|
|
135
|
+
/*#__PURE__*/ jsx(Card.Content, {
|
|
136
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
137
|
+
className: clsx('byline-sign-in-page-actions', sign_in_page_module.actions),
|
|
138
|
+
children: /*#__PURE__*/ jsx(Button, {
|
|
139
|
+
type: "button",
|
|
140
|
+
onClick: ()=>window.location.reload(),
|
|
141
|
+
children: t('common.actions.tryAgain')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
]
|
|
146
|
+
});
|
|
147
|
+
}
|
|
106
148
|
export { SignInPage };
|
|
@@ -2,6 +2,11 @@ import "./sign-in-page_module.css";
|
|
|
2
2
|
const sign_in_page_module = {
|
|
3
3
|
main: "main-Mz7TWj",
|
|
4
4
|
bar: "bar-uO4D8B",
|
|
5
|
-
inner: "inner-iPyHJh"
|
|
5
|
+
inner: "inner-iPyHJh",
|
|
6
|
+
checking: "checking-GccoM3",
|
|
7
|
+
srOnly: "srOnly-zH8cgE",
|
|
8
|
+
card: "card-TwlYhw",
|
|
9
|
+
alert: "alert-mFCzan",
|
|
10
|
+
actions: "actions-d9wIp6"
|
|
6
11
|
};
|
|
7
12
|
export default sign_in_page_module;
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
|
@@ -27,3 +27,52 @@
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
:is(.checking-GccoM3, .byline-sign-in-page-checking) {
|
|
31
|
+
color: inherit;
|
|
32
|
+
opacity: .6;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: center;
|
|
35
|
+
padding: 2rem 0;
|
|
36
|
+
display: flex;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.srOnly-zH8cgE {
|
|
40
|
+
clip: rect(0, 0, 0, 0);
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
border: 0;
|
|
43
|
+
width: 1px;
|
|
44
|
+
height: 1px;
|
|
45
|
+
margin: -1px;
|
|
46
|
+
padding: 0;
|
|
47
|
+
position: absolute;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:is(.card-TwlYhw, .byline-sign-in-page-card) {
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (min-width: 40rem) {
|
|
56
|
+
:is(.card-TwlYhw, .byline-sign-in-page-card) {
|
|
57
|
+
max-width: 380px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:is(.alert-mFCzan, .byline-sign-in-page-alert) {
|
|
62
|
+
width: 100%;
|
|
63
|
+
margin-top: var(--spacing-12);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (min-width: 40rem) {
|
|
67
|
+
:is(noscript > .alert-mFCzan, noscript > .byline-sign-in-page-alert) {
|
|
68
|
+
max-width: 380px;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:is(.actions-d9wIp6, .byline-sign-in-page-actions) {
|
|
73
|
+
padding-top: var(--spacing-8);
|
|
74
|
+
margin-bottom: var(--spacing-8);
|
|
75
|
+
justify-content: flex-end;
|
|
76
|
+
display: flex;
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
export declare function AdminSessionRecovery({ renewable }: {
|
|
9
|
+
renewable: boolean;
|
|
10
|
+
}): null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useRouter, useRouterState } from "@tanstack/react-router";
|
|
3
|
+
import { renewExpectedSession } from "./session-renewal.js";
|
|
4
|
+
const TERMINAL_CODES = new Set([
|
|
5
|
+
'ERR_UNAUTHENTICATED',
|
|
6
|
+
'ERR_INVALID_TOKEN',
|
|
7
|
+
'ERR_REVOKED_TOKEN',
|
|
8
|
+
'ERR_ACCOUNT_DISABLED',
|
|
9
|
+
'ERR_SESSION_CHANGED'
|
|
10
|
+
]);
|
|
11
|
+
let suppressed = false;
|
|
12
|
+
function AdminSessionRecovery({ renewable }) {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const href = useRouterState({
|
|
15
|
+
select: (state)=>state.location.href
|
|
16
|
+
});
|
|
17
|
+
const inFlight = useRef(false);
|
|
18
|
+
useEffect(()=>{
|
|
19
|
+
if (!renewable || inFlight.current || suppressed) return;
|
|
20
|
+
inFlight.current = true;
|
|
21
|
+
renewExpectedSession().then(()=>router.invalidate()).catch((error)=>{
|
|
22
|
+
if (TERMINAL_CODES.has(error?.code ?? '')) suppressed = true;
|
|
23
|
+
}).finally(()=>{
|
|
24
|
+
inFlight.current = false;
|
|
25
|
+
});
|
|
26
|
+
}, [
|
|
27
|
+
renewable,
|
|
28
|
+
href,
|
|
29
|
+
router
|
|
30
|
+
]);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export { AdminSessionRecovery };
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export declare function renewExpectedSession(): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { renewAdminSession } from "../server-fns/auth/renew.js";
|
|
2
|
+
import { coordinateAuthAction, expectedSession, flagSessionChanged, observeSession, renewSingleFlight } from "./session-coordination.js";
|
|
3
|
+
function renewExpectedSession() {
|
|
4
|
+
return renewSingleFlight(()=>coordinateAuthAction(async ()=>{
|
|
5
|
+
const result = await renewAdminSession({
|
|
6
|
+
data: {
|
|
7
|
+
expectedSessionId: expectedSession() ?? void 0
|
|
8
|
+
}
|
|
9
|
+
}).catch((error)=>{
|
|
10
|
+
if (error?.code === 'ERR_SESSION_CHANGED') flagSessionChanged();
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
observeSession(result.sessionId);
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
export { renewExpectedSession };
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export interface AdminLayoutGuardDeps<T> {
|
|
9
|
+
/** Load the full layout context; expected to verify the access credential first. */
|
|
10
|
+
load: () => Promise<T>;
|
|
11
|
+
/** Coordinated browser renewal. Resolves on success and rejects on any failure. */
|
|
12
|
+
renewSession: () => Promise<void>;
|
|
13
|
+
isBrowser: boolean;
|
|
14
|
+
/** True when the page has already flagged a session change awaiting acknowledgement. */
|
|
15
|
+
sessionChanged: () => boolean;
|
|
16
|
+
signInPath: string;
|
|
17
|
+
/** The requested location, preserved as `callbackUrl` on redirect. */
|
|
18
|
+
href: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function loadAdminLayoutContext<T>(deps: AdminLayoutGuardDeps<T>): Promise<T>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { redirect } from "@tanstack/react-router";
|
|
2
|
+
const AUTH_REDIRECT_CODES = new Set([
|
|
3
|
+
'ERR_UNAUTHENTICATED',
|
|
4
|
+
'ERR_ACCESS_EXPIRED',
|
|
5
|
+
'ERR_INVALID_TOKEN',
|
|
6
|
+
'ERR_REVOKED_TOKEN',
|
|
7
|
+
'ERR_ACCOUNT_DISABLED',
|
|
8
|
+
'ERR_SESSION_CHANGED'
|
|
9
|
+
]);
|
|
10
|
+
async function loadAdminLayoutContext(deps) {
|
|
11
|
+
const toSignIn = ()=>redirect({
|
|
12
|
+
to: deps.signInPath,
|
|
13
|
+
search: {
|
|
14
|
+
callbackUrl: deps.href
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
return await deps.load();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
const code = errorCode(error);
|
|
21
|
+
const changed = deps.isBrowser && deps.sessionChanged();
|
|
22
|
+
if (!changed && !AUTH_REDIRECT_CODES.has(code)) throw error;
|
|
23
|
+
if (changed || !deps.isBrowser || 'ERR_ACCESS_EXPIRED' !== code) throw toSignIn();
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await deps.renewSession();
|
|
27
|
+
} catch {
|
|
28
|
+
throw toSignIn();
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return await deps.load();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (AUTH_REDIRECT_CODES.has(errorCode(error))) throw toSignIn();
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function errorCode(error) {
|
|
38
|
+
return error?.code;
|
|
39
|
+
}
|
|
40
|
+
export { loadAdminLayoutContext };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
3
|
-
import { AnalyticsDashboard } from "@byline/admin/analytics/components/dashboard";
|
|
4
|
-
import { isAnalyticsDashboardPeriod } from "@byline/analytics/config";
|
|
3
|
+
import { ANALYTICS_PREVIEW_ROWS, AnalyticsDashboard } from "@byline/admin/analytics/components/dashboard";
|
|
4
|
+
import { ANALYTICS_MAX_TOP_LIMIT, isAnalyticsDashboardPeriod } from "@byline/analytics/config";
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { BreadcrumbsClient } from "../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js";
|
|
@@ -36,20 +36,20 @@ function createAdminAnalyticsRoute(path) {
|
|
|
36
36
|
data: {
|
|
37
37
|
...range,
|
|
38
38
|
kind: 'page',
|
|
39
|
-
limit:
|
|
39
|
+
limit: ANALYTICS_PREVIEW_ROWS
|
|
40
40
|
}
|
|
41
41
|
}),
|
|
42
42
|
getAnalyticsTop({
|
|
43
43
|
data: {
|
|
44
44
|
...range,
|
|
45
45
|
kind: 'download',
|
|
46
|
-
limit:
|
|
46
|
+
limit: ANALYTICS_PREVIEW_ROWS
|
|
47
47
|
}
|
|
48
48
|
}),
|
|
49
49
|
getAnalyticsReferrers({
|
|
50
50
|
data: {
|
|
51
51
|
...range,
|
|
52
|
-
limit:
|
|
52
|
+
limit: ANALYTICS_PREVIEW_ROWS
|
|
53
53
|
}
|
|
54
54
|
}),
|
|
55
55
|
getAnalyticsCountries({
|
|
@@ -90,6 +90,18 @@ function createAdminAnalyticsRoute(path) {
|
|
|
90
90
|
/*#__PURE__*/ jsx(AnalyticsDashboard, {
|
|
91
91
|
data: data,
|
|
92
92
|
period: period,
|
|
93
|
+
loadFullList: (kind)=>'referrers' === kind ? getAnalyticsReferrers({
|
|
94
|
+
data: {
|
|
95
|
+
...data.range,
|
|
96
|
+
limit: ANALYTICS_MAX_TOP_LIMIT
|
|
97
|
+
}
|
|
98
|
+
}) : getAnalyticsTop({
|
|
99
|
+
data: {
|
|
100
|
+
...data.range,
|
|
101
|
+
kind: 'pages' === kind ? 'page' : 'download',
|
|
102
|
+
limit: ANALYTICS_MAX_TOP_LIMIT
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
93
105
|
onPeriodChange: (nextPeriod)=>{
|
|
94
106
|
navigate({
|
|
95
107
|
to: getAdminRoutePath('analytics'),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
-
import { Outlet, createFileRoute
|
|
3
|
+
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
|
4
4
|
import { applyStoredTheme } from "@byline/admin/admin-account/components/theme";
|
|
5
5
|
import { BylineFieldServicesProvider } from "@byline/admin/react";
|
|
6
6
|
import { BylineAdminServicesProvider } from "@byline/admin/services";
|
|
@@ -20,47 +20,37 @@ import { bylineAdminServices } from "../integrations/byline-admin-services.js";
|
|
|
20
20
|
import { BylineAiAdminProvider } from "../integrations/byline-ai.js";
|
|
21
21
|
import { bylineFieldServices } from "../integrations/byline-field-services.js";
|
|
22
22
|
import { observeSession, sessionIsChanged } from "../integrations/session-coordination.js";
|
|
23
|
+
import { renewExpectedSession } from "../integrations/session-renewal.js";
|
|
23
24
|
import { getAnalyticsRuntime } from "../server-fns/analytics/index.js";
|
|
24
25
|
import { getCurrentAdminUser } from "../server-fns/auth/index.js";
|
|
25
26
|
import { getScheduledPublicationRuntime } from "../server-fns/collections/index.js";
|
|
26
27
|
import { getActiveLocaleFn, setAdminLocaleFn } from "../server-fns/i18n/index.js";
|
|
28
|
+
import { loadAdminLayoutContext } from "./admin-layout-guard.js";
|
|
27
29
|
import { getSignInRoutePath } from "./sign-in-path.js";
|
|
28
30
|
function createAdminLayoutRoute(path) {
|
|
29
31
|
const Route = createFileRoute(path)({
|
|
30
|
-
beforeLoad:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'ERR_REVOKED_TOKEN',
|
|
53
|
-
'ERR_ACCOUNT_DISABLED',
|
|
54
|
-
'ERR_SESSION_CHANGED'
|
|
55
|
-
].includes(code ?? '')) throw error;
|
|
56
|
-
throw redirect({
|
|
57
|
-
to: signInPath,
|
|
58
|
-
search: {
|
|
59
|
-
callbackUrl: location.href
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
},
|
|
32
|
+
beforeLoad: ({ location })=>loadAdminLayoutContext({
|
|
33
|
+
load: async ()=>{
|
|
34
|
+
const user = await getCurrentAdminUser();
|
|
35
|
+
if ("u" > typeof window) observeSession(user.sessionId);
|
|
36
|
+
const [activeLocale, scheduledPublicationRuntime, analyticsRuntime] = await Promise.all([
|
|
37
|
+
getActiveLocaleFn(),
|
|
38
|
+
getScheduledPublicationRuntime(),
|
|
39
|
+
getAnalyticsRuntime()
|
|
40
|
+
]);
|
|
41
|
+
return {
|
|
42
|
+
user,
|
|
43
|
+
activeLocale,
|
|
44
|
+
scheduledPublicationRuntime,
|
|
45
|
+
analyticsRuntime
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
renewSession: renewExpectedSession,
|
|
49
|
+
isBrowser: "u" > typeof window,
|
|
50
|
+
sessionChanged: sessionIsChanged,
|
|
51
|
+
signInPath: getSignInRoutePath(),
|
|
52
|
+
href: location.href
|
|
53
|
+
}),
|
|
64
54
|
component: function() {
|
|
65
55
|
const { user, activeLocale, scheduledPublicationRuntime, analyticsRuntime } = Route.useRouteContext();
|
|
66
56
|
const { i18n } = getAdminConfig();
|
|
@@ -28,11 +28,27 @@ export interface CurrentAdminUser {
|
|
|
28
28
|
}
|
|
29
29
|
export declare const getCurrentAdminUser: import("@tanstack/react-start").OptionalFetcher<undefined, undefined, Promise<CurrentAdminUser>>;
|
|
30
30
|
/**
|
|
31
|
-
* Soft
|
|
32
|
-
* renders only when the visitor is signed in).
|
|
31
|
+
* Soft variants for public-page consumers (e.g. the content admin bar that
|
|
32
|
+
* renders only when the visitor is signed in). Return `null` instead of
|
|
33
33
|
* throwing when there is no session — anonymous visitors are the common
|
|
34
34
|
* case here, so a thrown error would be wrong shape for a public route
|
|
35
35
|
* loader. Other failure modes (DB unavailable, session points to a
|
|
36
36
|
* deleted admin) are still coerced to `null` so the bar simply hides.
|
|
37
|
+
*
|
|
38
|
+
* `getCurrentAdminSessionSoft` additionally reports `renewable`: true when
|
|
39
|
+
* the access credential has expired but a refresh credential is present.
|
|
40
|
+
* Public reads never rotate credentials, so without this hint a signed-in
|
|
41
|
+
* editor's admin bar and preview mode silently vanish after the access
|
|
42
|
+
* lifetime. The host's `AdminSessionRecovery` component consumes it to run
|
|
43
|
+
* the coordinated browser renewal and re-run the page's loaders.
|
|
44
|
+
*
|
|
45
|
+
* `getCurrentAdminUserSoft` keeps the original `CurrentAdminUser | null`
|
|
46
|
+
* shape for existing integrations.
|
|
37
47
|
*/
|
|
48
|
+
export interface CurrentAdminSession {
|
|
49
|
+
user: CurrentAdminUser | null;
|
|
50
|
+
/** Access expired with a refresh credential present: the browser may renew and reload data. */
|
|
51
|
+
renewable: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare const getCurrentAdminSessionSoft: import("@tanstack/react-start").OptionalFetcher<undefined, undefined, Promise<CurrentAdminSession>>;
|
|
38
54
|
export declare const getCurrentAdminUserSoft: import("@tanstack/react-start").OptionalFetcher<undefined, undefined, Promise<CurrentAdminUser | null>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServerFn } from "@tanstack/react-start";
|
|
2
2
|
import { AuthError } from "@byline/auth";
|
|
3
|
-
import { getAdminRequestContext } from "@byline/client/server";
|
|
3
|
+
import { getAdminRequestContext, readRefreshTokenCookie } from "@byline/client/server";
|
|
4
4
|
import { bylineCore } from "../../integrations/byline-core.js";
|
|
5
5
|
const getCurrentAdminUser = createServerFn({
|
|
6
6
|
method: 'GET'
|
|
@@ -19,32 +19,49 @@ const getCurrentAdminUser = createServerFn({
|
|
|
19
19
|
abilities: Array.from(actor.abilities)
|
|
20
20
|
};
|
|
21
21
|
});
|
|
22
|
+
const getCurrentAdminSessionSoft = createServerFn({
|
|
23
|
+
method: 'GET'
|
|
24
|
+
}).handler(async ()=>resolveSoftSession());
|
|
22
25
|
const getCurrentAdminUserSoft = createServerFn({
|
|
23
26
|
method: 'GET'
|
|
24
|
-
}).handler(async ()=>
|
|
27
|
+
}).handler(async ()=>(await resolveSoftSession()).user);
|
|
28
|
+
const anonymous = {
|
|
29
|
+
user: null,
|
|
30
|
+
renewable: false
|
|
31
|
+
};
|
|
32
|
+
async function resolveSoftSession() {
|
|
25
33
|
try {
|
|
26
34
|
let actor;
|
|
27
35
|
let sessionId;
|
|
28
36
|
try {
|
|
29
37
|
({ actor, sessionId } = await getAdminRequestContext());
|
|
30
38
|
} catch (err) {
|
|
31
|
-
if (err instanceof AuthError)
|
|
39
|
+
if (err instanceof AuthError) {
|
|
40
|
+
const renewable = 'ERR_ACCESS_EXPIRED' === err.code && null != readRefreshTokenCookie();
|
|
41
|
+
return {
|
|
42
|
+
user: null,
|
|
43
|
+
renewable
|
|
44
|
+
};
|
|
45
|
+
}
|
|
32
46
|
throw err;
|
|
33
47
|
}
|
|
34
|
-
if (!actor) return
|
|
48
|
+
if (!actor) return anonymous;
|
|
35
49
|
const row = await bylineCore().adminStore?.adminUsers.getById(actor.id);
|
|
36
|
-
if (!row) return
|
|
50
|
+
if (!row) return anonymous;
|
|
37
51
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
user: {
|
|
53
|
+
sessionId: sessionId,
|
|
54
|
+
id: row.id,
|
|
55
|
+
email: row.email,
|
|
56
|
+
given_name: row.given_name,
|
|
57
|
+
family_name: row.family_name,
|
|
58
|
+
is_super_admin: row.is_super_admin,
|
|
59
|
+
abilities: Array.from(actor.abilities)
|
|
60
|
+
},
|
|
61
|
+
renewable: false
|
|
45
62
|
};
|
|
46
63
|
} catch {
|
|
47
|
-
return
|
|
64
|
+
return anonymous;
|
|
48
65
|
}
|
|
49
|
-
}
|
|
50
|
-
export { getCurrentAdminUser, getCurrentAdminUserSoft };
|
|
66
|
+
}
|
|
67
|
+
export { getCurrentAdminSessionSoft, getCurrentAdminUser, getCurrentAdminUserSoft };
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
export { type CurrentAdminUser, getCurrentAdminUser, getCurrentAdminUserSoft, } from './current-user.js';
|
|
8
|
+
export { type CurrentAdminSession, type CurrentAdminUser, getCurrentAdminSessionSoft, getCurrentAdminUser, getCurrentAdminUserSoft, } from './current-user.js';
|
|
9
9
|
export { adminSignIn, type SignInInput, type SignInResult } from './sign-in.js';
|
|
10
10
|
export { adminSignOut } from './sign-out.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { getCurrentAdminUser, getCurrentAdminUserSoft } from "./current-user.js";
|
|
1
|
+
export { getCurrentAdminSessionSoft, getCurrentAdminUser, getCurrentAdminUserSoft } from "./current-user.js";
|
|
2
2
|
export { adminSignIn } from "./sign-in.js";
|
|
3
3
|
export { adminSignOut } from "./sign-out.js";
|