@byline/host-tanstack-start 4.11.2 → 4.12.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/dist/admin-shell/admin-activity/list.js +2 -2
- package/dist/admin-shell/chrome/dashboard.js +2 -2
- package/dist/admin-shell/chrome/route-error.js +3 -3
- package/dist/admin-shell/chrome/sign-in-page.d.ts +5 -5
- package/dist/admin-shell/chrome/sign-in-page.js +8 -8
- package/dist/i18n/locale-cookie.d.ts +1 -1
- package/dist/i18n/locale-definitions.d.ts +1 -1
- package/dist/i18n/resolve-locale.js +5 -5
- package/dist/i18n/server-translator.js +2 -2
- package/dist/integrations/byline-admin-services.js +2 -2
- package/dist/routes/admin-path.js +2 -2
- package/dist/routes/create-admin-dashboard-route.js +2 -2
- package/dist/routes/create-admin-layout-route.d.ts +1 -6
- package/dist/routes/create-admin-layout-route.js +9 -10
- package/dist/routes/create-sign-in-route.d.ts +9 -1
- package/dist/routes/create-sign-in-route.js +5 -2
- package/dist/routes/get-content-locale-route-config.js +2 -2
- package/dist/routes/index.d.ts +2 -2
- package/dist/routes/index.js +1 -1
- package/dist/routes/sign-in-home-url.d.ts +9 -0
- package/dist/routes/sign-in-home-url.js +4 -0
- package/dist/routes/sign-in-path.d.ts +0 -4
- package/dist/routes/sign-in-path.js +3 -17
- package/dist/server-fns/auth/sign-in.js +1 -1
- package/dist/server-fns/i18n/index.d.ts +2 -2
- package/dist/server-fns/i18n/index.js +1 -1
- package/dist/server-fns/i18n/set-locale.d.ts +3 -3
- package/dist/server-fns/i18n/set-locale.js +4 -4
- package/package.json +8 -8
- package/src/admin-shell/admin-activity/list.tsx +2 -2
- package/src/admin-shell/chrome/dashboard.tsx +2 -2
- package/src/admin-shell/chrome/route-error.tsx +4 -4
- package/src/admin-shell/chrome/sign-in-page.tsx +12 -15
- package/src/i18n/locale-cookie.ts +1 -1
- package/src/i18n/locale-definitions.ts +1 -1
- package/src/i18n/resolve-locale.ts +6 -6
- package/src/i18n/server-translator.ts +2 -2
- package/src/integrations/byline-admin-services.ts +2 -2
- package/src/routes/admin-path.test.node.ts +3 -4
- package/src/routes/admin-path.ts +2 -2
- package/src/routes/auth-routes.test.node.ts +4 -10
- package/src/routes/create-admin-dashboard-route.tsx +2 -2
- package/src/routes/create-admin-layout-route.tsx +11 -17
- package/src/routes/create-sign-in-route.tsx +13 -2
- package/src/routes/get-content-locale-route-config.ts +2 -2
- package/src/routes/index.ts +5 -2
- package/src/routes/sign-in-home-url.test.node.ts +21 -0
- package/src/routes/sign-in-home-url.ts +12 -0
- package/src/routes/sign-in-path.test.node.ts +4 -61
- package/src/routes/sign-in-path.ts +2 -24
- package/src/server-fns/auth/sign-in.ts +2 -2
- package/src/server-fns/i18n/index.ts +2 -2
- package/src/server-fns/i18n/set-locale.ts +8 -8
|
@@ -8,19 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
import { SignInForm } from '@byline/admin/auth/components/sign-in-form'
|
|
10
10
|
import { BylineAdminServicesProvider } from '@byline/admin/services'
|
|
11
|
-
import {
|
|
11
|
+
import { getAdminConfig } from '@byline/core'
|
|
12
12
|
import type { LocaleCode } from '@byline/i18n'
|
|
13
13
|
import { I18nProvider, LanguageMenu } from '@byline/i18n/react'
|
|
14
14
|
import cx from 'clsx'
|
|
15
15
|
|
|
16
16
|
import { buildLocaleDefinitions } from '../../i18n/locale-definitions.js'
|
|
17
17
|
import { bylineAdminServices } from '../../integrations/byline-admin-services.js'
|
|
18
|
-
import {
|
|
18
|
+
import { setAdminLocaleFn } from '../../server-fns/i18n/index.js'
|
|
19
19
|
import styles from './sign-in-page.module.css'
|
|
20
20
|
|
|
21
21
|
interface SignInPageProps {
|
|
22
22
|
redirectTo: string
|
|
23
23
|
activeLocale: LocaleCode
|
|
24
|
+
homeUrl?: string
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -33,32 +34,28 @@ interface SignInPageProps {
|
|
|
33
34
|
* (the one that wraps the authenticated admin) doesn't apply here.
|
|
34
35
|
* `<LanguageMenu>` lights up automatically when two or more interface
|
|
35
36
|
* locales are configured. On change, the menu calls
|
|
36
|
-
* `
|
|
37
|
+
* `setAdminLocaleFn` which writes the cookie unconditionally and
|
|
37
38
|
* skips the DB write on the pre-auth path (the user has no admin
|
|
38
39
|
* session yet). After sign-in succeeds, the `adminSignIn` server fn
|
|
39
40
|
* reconciles the cookie locale into the user's
|
|
40
41
|
* `admin_users.preferred_locale` so the pre-auth choice becomes
|
|
41
42
|
* sticky across devices from day one.
|
|
42
43
|
*
|
|
43
|
-
* Threads
|
|
44
|
-
*
|
|
45
|
-
* button.
|
|
44
|
+
* Threads an optional host-owned `homeUrl` into `SignInForm` so the form's
|
|
45
|
+
* action row can render a plain "Home" link beside the submit button.
|
|
46
46
|
*/
|
|
47
|
-
export function SignInPage({ redirectTo, activeLocale }: SignInPageProps) {
|
|
48
|
-
const { i18n
|
|
49
|
-
const localeDefinitions = buildLocaleDefinitions(
|
|
50
|
-
i18n.interface.locales,
|
|
51
|
-
i18n.interface.localeDefinitions
|
|
52
|
-
)
|
|
47
|
+
export function SignInPage({ redirectTo, activeLocale, homeUrl }: SignInPageProps) {
|
|
48
|
+
const { i18n } = getAdminConfig()
|
|
49
|
+
const localeDefinitions = buildLocaleDefinitions(i18n.admin.locales, i18n.admin.localeDefinitions)
|
|
53
50
|
const handleSetLocale = async (next: LocaleCode) => {
|
|
54
|
-
await
|
|
51
|
+
await setAdminLocaleFn({ data: { locale: next } })
|
|
55
52
|
window.location.reload()
|
|
56
53
|
}
|
|
57
54
|
return (
|
|
58
55
|
<I18nProvider
|
|
59
56
|
bundle={i18n.translations ?? {}}
|
|
60
57
|
activeLocale={activeLocale}
|
|
61
|
-
defaultLocale={i18n.
|
|
58
|
+
defaultLocale={i18n.admin.defaultLocale}
|
|
62
59
|
localeDefinitions={localeDefinitions}
|
|
63
60
|
setLocale={handleSetLocale}
|
|
64
61
|
>
|
|
@@ -68,7 +65,7 @@ export function SignInPage({ redirectTo, activeLocale }: SignInPageProps) {
|
|
|
68
65
|
<LanguageMenu />
|
|
69
66
|
</div>
|
|
70
67
|
<div className={cx('byline-sign-in-page-inner', styles.inner)}>
|
|
71
|
-
<SignInForm redirectTo={redirectTo} homeUrl={
|
|
68
|
+
<SignInForm redirectTo={redirectTo} homeUrl={homeUrl} />
|
|
72
69
|
</div>
|
|
73
70
|
</main>
|
|
74
71
|
</BylineAdminServicesProvider>
|
|
@@ -44,7 +44,7 @@ export function readAdminLocaleCookie(): string | null {
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Write the admin locale cookie. Caller is expected to have validated
|
|
47
|
-
* `locale` against the permitted `i18n.
|
|
47
|
+
* `locale` against the permitted `i18n.admin.locales` set first.
|
|
48
48
|
*/
|
|
49
49
|
export function setAdminLocaleCookie(locale: string): void {
|
|
50
50
|
setCookie(ADMIN_LOCALE_COOKIE, locale, {
|
|
@@ -13,7 +13,7 @@ import type { LocaleDefinition } from '@byline/i18n'
|
|
|
13
13
|
* resolution order:
|
|
14
14
|
*
|
|
15
15
|
* 1. An entry from the host's configured `localeDefinitions` —
|
|
16
|
-
* `i18n.
|
|
16
|
+
* `i18n.admin.localeDefinitions` for the admin switcher, or
|
|
17
17
|
* `i18n.content.localeDefinitions` when a host frontend resolves
|
|
18
18
|
* content-locale labels (matched by code). Wins outright — this is
|
|
19
19
|
* the path that lets a host author write `Français` instead of the
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* Server-side locale resolution for the admin interface.
|
|
11
11
|
*
|
|
12
|
-
* Combines `@byline/i18n`'s pure `
|
|
12
|
+
* Combines `@byline/i18n`'s pure `resolveAdminLocale` cascade with
|
|
13
13
|
* the host's request-scoped signals — the `byline_admin_lng` cookie, the
|
|
14
14
|
* `Accept-Language` header, and (when an admin session is present)
|
|
15
15
|
* `admin_users.preferred_locale` on the authenticated actor.
|
|
@@ -25,7 +25,7 @@ import { getRequestHeader } from '@tanstack/react-start/server'
|
|
|
25
25
|
import { AuthError, AuthErrorCodes } from '@byline/auth'
|
|
26
26
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
27
27
|
import type { LocaleCode } from '@byline/i18n'
|
|
28
|
-
import {
|
|
28
|
+
import { resolveAdminLocale } from '@byline/i18n'
|
|
29
29
|
|
|
30
30
|
import { bylineCore } from '../integrations/byline-core.js'
|
|
31
31
|
import { readAdminLocaleCookie } from './locale-cookie.js'
|
|
@@ -43,16 +43,16 @@ export async function resolveRequestLocale(options?: {
|
|
|
43
43
|
skipActorLookup?: boolean
|
|
44
44
|
}): Promise<LocaleCode> {
|
|
45
45
|
const core = bylineCore()
|
|
46
|
-
const {
|
|
46
|
+
const { admin: adminConfig } = core.config.i18n
|
|
47
47
|
|
|
48
48
|
let preferred: string | null = null
|
|
49
49
|
if (!options?.skipActorLookup) {
|
|
50
50
|
preferred = await readPreferredLocaleFromActor()
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
return
|
|
54
|
-
locales:
|
|
55
|
-
defaultLocale:
|
|
53
|
+
return resolveAdminLocale({
|
|
54
|
+
locales: adminConfig.locales,
|
|
55
|
+
defaultLocale: adminConfig.defaultLocale,
|
|
56
56
|
preferred,
|
|
57
57
|
cookie: readAdminLocaleCookie(),
|
|
58
58
|
acceptLanguage: readAcceptLanguage(),
|
|
@@ -39,7 +39,7 @@ export interface ServerTranslator {
|
|
|
39
39
|
|
|
40
40
|
export async function resolveServerTranslator(namespace: string): Promise<ServerTranslator> {
|
|
41
41
|
const core = bylineCore()
|
|
42
|
-
const {
|
|
42
|
+
const { admin: adminConfig, translations: bundle } = core.config.i18n
|
|
43
43
|
if (bundle == null) {
|
|
44
44
|
throw new Error(
|
|
45
45
|
'[resolveServerTranslator] no translation bundle is registered. ' +
|
|
@@ -51,7 +51,7 @@ export async function resolveServerTranslator(namespace: string): Promise<Server
|
|
|
51
51
|
const formatter = createFormatter({
|
|
52
52
|
bundle,
|
|
53
53
|
activeLocale,
|
|
54
|
-
defaultLocale:
|
|
54
|
+
defaultLocale: adminConfig.defaultLocale,
|
|
55
55
|
})
|
|
56
56
|
return {
|
|
57
57
|
t: (key, values) => formatter.t(namespace, key, values),
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from '../server-fns/admin-users/index.js'
|
|
36
36
|
import { adminSignIn } from '../server-fns/auth/index.js'
|
|
37
37
|
import { getCollectionDocumentVersion as serverGetCollectionDocumentVersion } from '../server-fns/collections/get.js'
|
|
38
|
-
import {
|
|
38
|
+
import { setAdminLocaleFn } from '../server-fns/i18n/index.js'
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Diff helper adapter — the contract uses positional args; the underlying
|
|
@@ -67,7 +67,7 @@ export const bylineAdminServices: BylineAdminServices = {
|
|
|
67
67
|
// Account self-service
|
|
68
68
|
updateAccount,
|
|
69
69
|
changeAccountPassword,
|
|
70
|
-
|
|
70
|
+
setAdminLocale: setAdminLocaleFn,
|
|
71
71
|
|
|
72
72
|
// Admin user writes
|
|
73
73
|
createAdminUser,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAdminConfig } from '@byline/core'
|
|
2
2
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -11,13 +11,12 @@ import {
|
|
|
11
11
|
} from './admin-path.js'
|
|
12
12
|
|
|
13
13
|
function registerRoutes(admin?: string): void {
|
|
14
|
-
|
|
15
|
-
serverURL: 'http://localhost:3000',
|
|
14
|
+
defineAdminConfig({
|
|
16
15
|
routes: admin ? { admin } : undefined,
|
|
17
16
|
collections: [],
|
|
18
17
|
admin: [],
|
|
19
18
|
i18n: {
|
|
20
|
-
|
|
19
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
21
20
|
content: { defaultLocale: 'en', locales: [] },
|
|
22
21
|
},
|
|
23
22
|
})
|
package/src/routes/admin-path.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { getAdminConfig, normalizeRootRelativeRedirect } from '@byline/core'
|
|
10
10
|
|
|
11
11
|
/** Build an admin URL or parameterized TanStack route from client-safe config. */
|
|
12
12
|
export function getAdminRoutePath(...segments: ReadonlyArray<string | number>): string {
|
|
13
|
-
const configuredBase =
|
|
13
|
+
const configuredBase = getAdminConfig().routes.admin
|
|
14
14
|
const normalizedBase = configuredBase.replace(/^\/+|\/+$/g, '')
|
|
15
15
|
const base = normalizedBase ? `/${normalizedBase}` : '/'
|
|
16
16
|
const suffix = segments
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAdminConfig } from '@byline/core'
|
|
2
2
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { resolveAdminCallbackPath, resolveAdminSignInRedirect } from './admin-path.js'
|
|
5
|
-
import {
|
|
5
|
+
import { getSignInRoutePath } from './sign-in-path.js'
|
|
6
6
|
|
|
7
7
|
describe('admin authentication routes', () => {
|
|
8
8
|
beforeEach(() => {
|
|
9
|
-
|
|
10
|
-
serverURL: 'http://localhost:3000',
|
|
9
|
+
defineAdminConfig({
|
|
11
10
|
routes: { admin: '/cms/', signIn: '/staff/login/' },
|
|
12
11
|
collections: [],
|
|
13
12
|
admin: [],
|
|
14
13
|
i18n: {
|
|
15
|
-
|
|
14
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
16
15
|
content: { defaultLocale: 'en', locales: [] },
|
|
17
16
|
},
|
|
18
17
|
})
|
|
@@ -30,10 +29,5 @@ describe('admin authentication routes', () => {
|
|
|
30
29
|
|
|
31
30
|
it('shares routes.signIn between guard and sign-out consumers', () => {
|
|
32
31
|
expect(getSignInRoutePath()).toBe('/staff/login')
|
|
33
|
-
expect(configureSignInRoutePath('/staff/login/')).toBe('/staff/login')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('does not allow the deprecated layout override to silently diverge', () => {
|
|
37
|
-
expect(() => configureSignInRoutePath('/auth/login')).toThrow(/must match routes\.signIn/)
|
|
38
32
|
})
|
|
39
33
|
})
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { createFileRoute } from '@tanstack/react-router'
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { getAdminConfig } from '@byline/core'
|
|
12
12
|
import { useTranslation } from '@byline/i18n/react'
|
|
13
13
|
|
|
14
14
|
import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
|
|
@@ -20,7 +20,7 @@ export function createAdminDashboardRoute(path: string) {
|
|
|
20
20
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic path bypasses route-tree typing
|
|
21
21
|
const Route: any = createFileRoute(path as never)({
|
|
22
22
|
loader: async () => {
|
|
23
|
-
const { collections } =
|
|
23
|
+
const { collections } = getAdminConfig()
|
|
24
24
|
const statsMap: Record<string, CollectionStatusCount[]> = {}
|
|
25
25
|
|
|
26
26
|
await Promise.all(
|
|
@@ -25,7 +25,7 @@ import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
|
|
25
25
|
import { applyStoredTheme } from '@byline/admin/admin-account/components/theme'
|
|
26
26
|
import { BylineFieldServicesProvider } from '@byline/admin/react'
|
|
27
27
|
import { BylineAdminServicesProvider } from '@byline/admin/services'
|
|
28
|
-
import {
|
|
28
|
+
import { getAdminConfig } from '@byline/core'
|
|
29
29
|
import type { LocaleCode } from '@byline/i18n'
|
|
30
30
|
import { I18nProvider } from '@byline/i18n/react'
|
|
31
31
|
import cx from 'clsx'
|
|
@@ -42,19 +42,13 @@ import { bylineAdminServices } from '../integrations/byline-admin-services.js'
|
|
|
42
42
|
import { BylineAiAdminProvider } from '../integrations/byline-ai.js'
|
|
43
43
|
import { bylineFieldServices } from '../integrations/byline-field-services.js'
|
|
44
44
|
import { getCurrentAdminUser } from '../server-fns/auth/index.js'
|
|
45
|
-
import { getActiveLocaleFn,
|
|
46
|
-
import {
|
|
45
|
+
import { getActiveLocaleFn, setAdminLocaleFn } from '../server-fns/i18n/index.js'
|
|
46
|
+
import { getSignInRoutePath } from './sign-in-path.js'
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
/** @deprecated Configure `routes.signIn`; an override must resolve to the same path. */
|
|
50
|
-
signInPath?: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function createAdminLayoutRoute(path: string, opts: AdminLayoutOpts = {}) {
|
|
54
|
-
const resolveSignInPath = createSignInRoutePathResolver(opts.signInPath)
|
|
48
|
+
export function createAdminLayoutRoute(path: string) {
|
|
55
49
|
const Route: any = createFileRoute(path as never)({
|
|
56
50
|
beforeLoad: async ({ location }: { location: { href: string } }) => {
|
|
57
|
-
const signInPath =
|
|
51
|
+
const signInPath = getSignInRoutePath()
|
|
58
52
|
try {
|
|
59
53
|
const user = await getCurrentAdminUser()
|
|
60
54
|
// Resolve the active interface locale once on the server so SSR
|
|
@@ -81,10 +75,10 @@ export function createAdminLayoutRoute(path: string, opts: AdminLayoutOpts = {})
|
|
|
81
75
|
user: Awaited<ReturnType<typeof getCurrentAdminUser>>
|
|
82
76
|
activeLocale: LocaleCode
|
|
83
77
|
}
|
|
84
|
-
const { i18n } =
|
|
78
|
+
const { i18n } = getAdminConfig()
|
|
85
79
|
const localeDefinitions = buildLocaleDefinitions(
|
|
86
|
-
i18n.
|
|
87
|
-
i18n.
|
|
80
|
+
i18n.admin.locales,
|
|
81
|
+
i18n.admin.localeDefinitions
|
|
88
82
|
)
|
|
89
83
|
// Re-assert the admin user's stored theme when the shell mounts.
|
|
90
84
|
// Byline shares the host's theme contract, but a host theme provider
|
|
@@ -93,18 +87,18 @@ export function createAdminLayoutRoute(path: string, opts: AdminLayoutOpts = {})
|
|
|
93
87
|
useEffect(() => {
|
|
94
88
|
applyStoredTheme()
|
|
95
89
|
}, [])
|
|
96
|
-
// Cookie + DB write happen in
|
|
90
|
+
// Cookie + DB write happen in setAdminLocaleFn; full reload
|
|
97
91
|
// re-runs beforeLoad so the provider re-renders with the new
|
|
98
92
|
// bundle/locale (no in-place bundle swap needed for PR 1's scope).
|
|
99
93
|
const handleSetLocale = async (next: LocaleCode) => {
|
|
100
|
-
await
|
|
94
|
+
await setAdminLocaleFn({ data: { locale: next } })
|
|
101
95
|
window.location.reload()
|
|
102
96
|
}
|
|
103
97
|
return (
|
|
104
98
|
<I18nProvider
|
|
105
99
|
bundle={i18n.translations ?? {}}
|
|
106
100
|
activeLocale={activeLocale}
|
|
107
|
-
defaultLocale={i18n.
|
|
101
|
+
defaultLocale={i18n.admin.defaultLocale}
|
|
108
102
|
localeDefinitions={localeDefinitions}
|
|
109
103
|
setLocale={handleSetLocale}
|
|
110
104
|
>
|
|
@@ -22,13 +22,23 @@ import type { LocaleCode } from '@byline/i18n'
|
|
|
22
22
|
import { SignInPage } from '../admin-shell/chrome/sign-in-page.js'
|
|
23
23
|
import { getActiveLocaleFn } from '../server-fns/i18n/index.js'
|
|
24
24
|
import { resolveAdminCallbackPath, resolveAdminSignInRedirect } from './admin-path.js'
|
|
25
|
+
import { resolveSignInHomeUrl } from './sign-in-home-url.js'
|
|
25
26
|
|
|
26
27
|
interface SignInSearch {
|
|
27
28
|
callbackUrl?: string
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export
|
|
31
|
-
|
|
31
|
+
export interface CreateSignInRouteOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Client-safe URL for the sign-in page's Home link. Defaults to `/`, which
|
|
34
|
+
* preserves the link for integrated hosts without requiring an absolute site
|
|
35
|
+
* origin. Pass an absolute URL when the public home is on another origin.
|
|
36
|
+
*/
|
|
37
|
+
homeUrl?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createSignInRoute(path: string, { homeUrl }: CreateSignInRouteOptions = {}) {
|
|
41
|
+
const resolvedHomeUrl = resolveSignInHomeUrl(homeUrl)
|
|
32
42
|
const Route: any = createFileRoute(path as never)({
|
|
33
43
|
validateSearch: (search: Record<string, unknown>): SignInSearch => {
|
|
34
44
|
const callbackUrl = resolveAdminCallbackPath(search.callbackUrl)
|
|
@@ -50,6 +60,7 @@ export function createSignInRoute(path: string) {
|
|
|
50
60
|
<SignInPage
|
|
51
61
|
redirectTo={resolveAdminSignInRedirect(callbackUrl)}
|
|
52
62
|
activeLocale={activeLocale}
|
|
63
|
+
homeUrl={resolvedHomeUrl}
|
|
53
64
|
/>
|
|
54
65
|
)
|
|
55
66
|
},
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { getAdminConfig } from '@byline/core'
|
|
10
10
|
|
|
11
11
|
import type { ContentLocaleOption } from '../admin-shell/collections/view-menu.js'
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ export function getContentLocaleRouteConfig(): {
|
|
|
14
14
|
contentLocales: ContentLocaleOption[]
|
|
15
15
|
defaultContentLocale: string
|
|
16
16
|
} {
|
|
17
|
-
const content =
|
|
17
|
+
const content = getAdminConfig().i18n.content
|
|
18
18
|
const labels = new Map(
|
|
19
19
|
content.localeDefinitions?.map(({ code, nativeName }) => [code, nativeName]) ?? []
|
|
20
20
|
)
|
package/src/routes/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ export {
|
|
|
14
14
|
resolveAdminCallbackPath,
|
|
15
15
|
resolveAdminSignInRedirect,
|
|
16
16
|
} from './admin-path.js'
|
|
17
|
-
export {
|
|
17
|
+
export { getSignInRoutePath } from './sign-in-path.js'
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Route factories for the Byline admin UI on TanStack Start.
|
|
@@ -45,4 +45,7 @@ export { createCollectionCreateRoute } from './create-collection-create-route.js
|
|
|
45
45
|
export { createCollectionEditRoute } from './create-collection-edit-route.js'
|
|
46
46
|
export { createCollectionHistoryRoute } from './create-collection-history-route.js'
|
|
47
47
|
export { createCollectionListRoute } from './create-collection-list-route.js'
|
|
48
|
-
export {
|
|
48
|
+
export {
|
|
49
|
+
type CreateSignInRouteOptions,
|
|
50
|
+
createSignInRoute,
|
|
51
|
+
} from './create-sign-in-route.js'
|
|
@@ -0,0 +1,21 @@
|
|
|
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 { resolveSignInHomeUrl } from './sign-in-home-url.js'
|
|
12
|
+
|
|
13
|
+
describe('resolveSignInHomeUrl', () => {
|
|
14
|
+
it('defaults the sign-in Home link to the same-origin root', () => {
|
|
15
|
+
expect(resolveSignInHomeUrl(undefined)).toBe('/')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('preserves an explicit host-owned Home URL', () => {
|
|
19
|
+
expect(resolveSignInHomeUrl('https://example.test/')).toBe('https://example.test/')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
/** Resolve the sign-in Home link without requiring a configured site origin. */
|
|
10
|
+
export function resolveSignInHomeUrl(homeUrl: string | undefined): string {
|
|
11
|
+
return homeUrl ?? '/'
|
|
12
|
+
}
|
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAdminConfig } from '@byline/core'
|
|
2
2
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
configureSignInRoutePath,
|
|
6
|
-
createSignInRoutePathResolver,
|
|
7
|
-
getSignInRoutePath,
|
|
8
|
-
} from './sign-in-path.js'
|
|
4
|
+
import { getSignInRoutePath } from './sign-in-path.js'
|
|
9
5
|
|
|
10
6
|
describe('sign-in route configuration', () => {
|
|
11
7
|
beforeEach(() => {
|
|
12
|
-
|
|
13
|
-
serverURL: 'http://localhost:3000',
|
|
8
|
+
defineAdminConfig({
|
|
14
9
|
routes: { signIn: '/staff/login/' },
|
|
15
10
|
collections: [],
|
|
16
11
|
admin: [],
|
|
17
12
|
i18n: {
|
|
18
|
-
|
|
13
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
19
14
|
content: { defaultLocale: 'en', locales: [] },
|
|
20
15
|
},
|
|
21
16
|
})
|
|
@@ -24,56 +19,4 @@ describe('sign-in route configuration', () => {
|
|
|
24
19
|
it('reads and normalizes routes.signIn', () => {
|
|
25
20
|
expect(getSignInRoutePath()).toBe('/staff/login')
|
|
26
21
|
})
|
|
27
|
-
|
|
28
|
-
it('allows a deprecated signInPath override when it matches canonical config', () => {
|
|
29
|
-
expect(configureSignInRoutePath('staff/login/')).toBe('/staff/login')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('rejects an invalid or divergent deprecated override when resolved', () => {
|
|
33
|
-
expect(() => configureSignInRoutePath('/auth/login/')).toThrow(/must match routes\.signIn/)
|
|
34
|
-
expect(() => configureSignInRoutePath('/auth/../login')).toThrow(/routes\.signIn/)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('validates an override against configured rather than default route trees', () => {
|
|
38
|
-
defineClientConfig({
|
|
39
|
-
serverURL: 'http://localhost:3000',
|
|
40
|
-
routes: { admin: '/cms', api: '/rpc', signIn: '/admin/login' },
|
|
41
|
-
collections: [],
|
|
42
|
-
admin: [],
|
|
43
|
-
i18n: {
|
|
44
|
-
interface: { defaultLocale: 'en', locales: [] },
|
|
45
|
-
content: { defaultLocale: 'en', locales: [] },
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
expect(configureSignInRoutePath('/admin/login/')).toBe('/admin/login')
|
|
50
|
-
expect(() => configureSignInRoutePath('/cms/login')).toThrow(/routes\.signIn/)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('defers a deprecated override until parent beforeLoad config registration', () => {
|
|
54
|
-
defineClientConfig({
|
|
55
|
-
serverURL: 'http://localhost:3000',
|
|
56
|
-
routes: { signIn: '/old/login' },
|
|
57
|
-
collections: [],
|
|
58
|
-
admin: [],
|
|
59
|
-
i18n: {
|
|
60
|
-
interface: { defaultLocale: 'en', locales: [] },
|
|
61
|
-
content: { defaultLocale: 'en', locales: [] },
|
|
62
|
-
},
|
|
63
|
-
})
|
|
64
|
-
const resolveSignInPath = createSignInRoutePathResolver('/staff/login/')
|
|
65
|
-
|
|
66
|
-
defineClientConfig({
|
|
67
|
-
serverURL: 'http://localhost:3000',
|
|
68
|
-
routes: { signIn: '/staff/login' },
|
|
69
|
-
collections: [],
|
|
70
|
-
admin: [],
|
|
71
|
-
i18n: {
|
|
72
|
-
interface: { defaultLocale: 'en', locales: [] },
|
|
73
|
-
content: { defaultLocale: 'en', locales: [] },
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
expect(resolveSignInPath()).toBe('/staff/login')
|
|
78
|
-
})
|
|
79
22
|
})
|
|
@@ -6,31 +6,9 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { getAdminConfig } from '@byline/core'
|
|
10
10
|
|
|
11
11
|
/** Read the canonical client-safe sign-in destination. */
|
|
12
12
|
export function getSignInRoutePath(): string {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** @deprecated Configure `routes.signIn`; retained for route-factory input validation. */
|
|
17
|
-
export function configureSignInRoutePath(legacyOverride?: string): string {
|
|
18
|
-
const routes = getClientConfig().routes
|
|
19
|
-
const configured = routes.signIn
|
|
20
|
-
if (legacyOverride === undefined) return configured
|
|
21
|
-
|
|
22
|
-
const normalizedOverride = resolveRoutes({ ...routes, signIn: legacyOverride }).signIn
|
|
23
|
-
if (normalizedOverride !== configured) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
`createAdminLayoutRoute signInPath (${normalizedOverride}) must match routes.signIn (${configured})`
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
return configured
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Defer deprecated override validation until the admin route's beforeLoad boundary. */
|
|
32
|
-
export function createSignInRoutePathResolver(legacyOverride?: string): () => string {
|
|
33
|
-
return legacyOverride === undefined
|
|
34
|
-
? getSignInRoutePath
|
|
35
|
-
: () => configureSignInRoutePath(legacyOverride)
|
|
13
|
+
return getAdminConfig().routes.signIn
|
|
36
14
|
}
|
|
@@ -91,7 +91,7 @@ export const adminSignIn = createServerFn({ method: 'POST' })
|
|
|
91
91
|
*
|
|
92
92
|
* - The cookie is unset (the cascade falls through to the existing
|
|
93
93
|
* column / Accept-Language / default anyway).
|
|
94
|
-
* - The cookie carries a locale outside `i18n.
|
|
94
|
+
* - The cookie carries a locale outside `i18n.admin.locales`
|
|
95
95
|
* (stale value pointing at a removed locale — let the resolver
|
|
96
96
|
* fall through cleanly).
|
|
97
97
|
* - No admin store is configured (headless tooling paths).
|
|
@@ -102,7 +102,7 @@ async function reconcileLocaleAfterSignIn(adminUserId: string): Promise<void> {
|
|
|
102
102
|
if (cookieLocale == null) return
|
|
103
103
|
|
|
104
104
|
const core = bylineCore()
|
|
105
|
-
const locales = core.config.i18n.
|
|
105
|
+
const locales = core.config.i18n.admin.locales
|
|
106
106
|
if (!locales.includes(cookieLocale)) return
|
|
107
107
|
|
|
108
108
|
const adminStore = core.adminStore
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { getActiveLocaleFn } from './get-active-locale.js'
|
|
10
|
-
export {
|
|
11
|
-
export type {
|
|
10
|
+
export { setAdminLocaleFn } from './set-locale.js'
|
|
11
|
+
export type { SetAdminLocaleInput, SetAdminLocaleResult } from './set-locale.js'
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* is also updated so the choice follows the user across devices.
|
|
16
16
|
*
|
|
17
17
|
* Locale validation is host-side: the request locale must appear in
|
|
18
|
-
* `config.i18n.
|
|
18
|
+
* `config.i18n.admin.locales` (or be `null`, which clears the
|
|
19
19
|
* cookie + column and re-engages the detection cascade). Validation
|
|
20
20
|
* lives here rather than in the admin module so the rule stays close
|
|
21
21
|
* to the host's i18n config.
|
|
@@ -35,12 +35,12 @@ import { getAdminRequestContext } from '@byline/client/server'
|
|
|
35
35
|
import { clearAdminLocaleCookie, setAdminLocaleCookie } from '../../i18n/locale-cookie.js'
|
|
36
36
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
37
37
|
|
|
38
|
-
export interface
|
|
38
|
+
export interface SetAdminLocaleInput {
|
|
39
39
|
/** BCP 47 tag, or `null` to clear the preference. */
|
|
40
40
|
locale: string | null
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export interface
|
|
43
|
+
export interface SetAdminLocaleResult {
|
|
44
44
|
ok: true
|
|
45
45
|
/** Echo of the persisted value. `null` means the column was cleared. */
|
|
46
46
|
locale: string | null
|
|
@@ -53,17 +53,17 @@ export interface SetInterfaceLocaleResult {
|
|
|
53
53
|
account: AccountResponse | null
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export const
|
|
57
|
-
.validator((input:
|
|
58
|
-
.handler(async ({ data }): Promise<
|
|
56
|
+
export const setAdminLocaleFn = createServerFn({ method: 'POST' })
|
|
57
|
+
.validator((input: SetAdminLocaleInput) => input)
|
|
58
|
+
.handler(async ({ data }): Promise<SetAdminLocaleResult> => {
|
|
59
59
|
const core = bylineCore()
|
|
60
|
-
const locales = core.config.i18n.
|
|
60
|
+
const locales = core.config.i18n.admin.locales
|
|
61
61
|
|
|
62
62
|
// Validate against the permitted set. `null` is always permitted —
|
|
63
63
|
// it's the "use detection" signal.
|
|
64
64
|
if (data.locale != null && !locales.includes(data.locale)) {
|
|
65
65
|
throw new Error(
|
|
66
|
-
`[
|
|
66
|
+
`[setAdminLocaleFn] locale '${data.locale}' is not in i18n.admin.locales [${locales.join(', ')}].`
|
|
67
67
|
)
|
|
68
68
|
}
|
|
69
69
|
|