From 7326a1855f1fc82e1b10ab4072a654ef4bfcf343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Pe=CC=81rard?= Date: Wed, 22 Apr 2026 16:00:19 +0200 Subject: [PATCH 1/6] get session beforeLoad to remove full spinner --- src/components/ui/theme-switcher.tsx | 10 ++----- src/features/account/change-name-drawer.tsx | 4 +-- src/features/account/user-card.tsx | 4 +-- src/features/auth/guard-authenticated.tsx | 5 ++-- src/features/auth/guard-public-only.tsx | 4 +-- src/features/auth/page-login-verify.tsx | 3 ++- src/features/auth/page-logout.tsx | 5 ++-- src/features/auth/page-onboarding.tsx | 4 +-- src/features/auth/session.server.ts | 20 ++++++++++++++ src/features/auth/use-session.ts | 26 +++++++++++++++++++ src/features/auth/utils.ts | 3 ++- src/features/auth/with-permissions.tsx | 3 ++- src/features/demo/demo-app-switch.tsx | 3 ++- src/features/user/manager/form-user.tsx | 4 +-- .../user/manager/page-user-update.tsx | 4 +-- src/features/user/manager/page-user.tsx | 8 +++--- src/layout/manager/nav-user.tsx | 11 +++++--- src/routes/__root.tsx | 14 +++++++++- src/routes/index.tsx | 7 +++-- 19 files changed, 102 insertions(+), 40 deletions(-) create mode 100644 src/features/auth/session.server.ts create mode 100644 src/features/auth/use-session.ts diff --git a/src/components/ui/theme-switcher.tsx b/src/components/ui/theme-switcher.tsx index 71f4534d3..b633f0349 100644 --- a/src/components/ui/theme-switcher.tsx +++ b/src/components/ui/theme-switcher.tsx @@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next'; import { match } from 'ts-pattern'; import { cn } from '@/lib/tailwind/utils'; -import { useHydrated } from '@/hooks/use-hydrated'; import { Button } from '@/components/ui/button'; import { @@ -25,11 +24,6 @@ export const themes = ['system', 'light', 'dark'] as const; export const ThemeSwitcher = (props: { iconOnly?: boolean }) => { const { t } = useTranslation(['common']); const { theme, setTheme } = useTheme(); - const hydrated = useHydrated(); - - if (!hydrated) { - return
; - } return ( @@ -41,13 +35,13 @@ export const ThemeSwitcher = (props: { iconOnly?: boolean }) => { /> } > - {match(theme as (typeof themes)[number]) + {match((theme as (typeof themes)[number]) ?? 'system') .with('system', () => ) .with('light', () => ) .with('dark', () => ) .exhaustive()} - {match(theme as (typeof themes)[number]) + {match((theme as (typeof themes)[number]) ?? 'system') .with('system', () => t('common:themes.values.system')) .with('light', () => t('common:themes.values.light')) .with('dark', () => t('common:themes.values.dark')) diff --git a/src/features/account/change-name-drawer.tsx b/src/features/account/change-name-drawer.tsx index 00c3061fa..613b4e807 100644 --- a/src/features/account/change-name-drawer.tsx +++ b/src/features/account/change-name-drawer.tsx @@ -29,12 +29,12 @@ import { FormFieldsAccountUpdateName, zFormFieldsAccountUpdateName, } from '@/features/account/schema'; -import { authClient } from '@/features/auth/client'; +import { useSession } from '@/features/auth/use-session'; export const ChangeNameDrawer = (props: { children: ReactElement }) => { const { t } = useTranslation(['account']); const [open, setOpen] = useState(false); - const session = authClient.useSession(); + const session = useSession(); const form = useForm({ resolver: zodResolver(zFormFieldsAccountUpdateName()), values: { diff --git a/src/features/account/user-card.tsx b/src/features/account/user-card.tsx index 751169968..460418c7f 100644 --- a/src/features/account/user-card.tsx +++ b/src/features/account/user-card.tsx @@ -8,12 +8,12 @@ import { Card, CardAction, CardHeader, CardTitle } from '@/components/ui/card'; import { AccountCardRow } from '@/features/account/account-card-row'; import { ChangeNameDrawer } from '@/features/account/change-name-drawer'; -import { authClient } from '@/features/auth/client'; import { ConfirmSignOut } from '@/features/auth/confirm-signout'; +import { useSession } from '@/features/auth/use-session'; export const UserCard = () => { const { t } = useTranslation(['auth', 'account']); - const session = authClient.useSession(); + const session = useSession(); return ( diff --git a/src/features/auth/guard-authenticated.tsx b/src/features/auth/guard-authenticated.tsx index 9f355ae98..7dd2e6660 100644 --- a/src/features/auth/guard-authenticated.tsx +++ b/src/features/auth/guard-authenticated.tsx @@ -7,6 +7,7 @@ import { Spinner } from '@/components/ui/spinner'; import { authClient } from '@/features/auth/client'; import { PageOnboarding } from '@/features/auth/page-onboarding'; import { Permission, Role } from '@/features/auth/permissions'; +import { useSession } from '@/features/auth/use-session'; export const GuardAuthenticated = ({ children, @@ -15,7 +16,7 @@ export const GuardAuthenticated = ({ children?: ReactNode; permissionApps?: Permission['apps']; }) => { - const session = authClient.useSession(); + const session = useSession(); const router = useRouter(); if (session.isPending) { @@ -37,12 +38,10 @@ export const GuardAuthenticated = ({ return null; } - // Check if onboarding is done if (!session.data.user.onboardedAt) { return ; } - // Unauthorized if the user permission do not match if ( permissionApps && !authClient.admin.checkRolePermission({ diff --git a/src/features/auth/guard-public-only.tsx b/src/features/auth/guard-public-only.tsx index 38430bae8..eb35eadb1 100644 --- a/src/features/auth/guard-public-only.tsx +++ b/src/features/auth/guard-public-only.tsx @@ -3,11 +3,11 @@ import { Activity, ReactNode } from 'react'; import { PageError } from '@/components/errors/page-error'; import { Spinner } from '@/components/ui/spinner'; -import { authClient } from '@/features/auth/client'; +import { useSession } from '@/features/auth/use-session'; import { useRedirectAfterLogin } from '@/features/auth/utils'; export const GuardPublicOnly = ({ children }: { children?: ReactNode }) => { - const session = authClient.useSession(); + const session = useSession(); useRedirectAfterLogin(); if (session.error && session.error.status > 0) { diff --git a/src/features/auth/page-login-verify.tsx b/src/features/auth/page-login-verify.tsx index 27ab0fbea..f2077b996 100644 --- a/src/features/auth/page-login-verify.tsx +++ b/src/features/auth/page-login-verify.tsx @@ -24,6 +24,7 @@ import { FormFieldsLoginVerify, zFormFieldsLoginVerify, } from '@/features/auth/schema'; +import { useSession } from '@/features/auth/use-session'; import { LoginEmailOtpHint } from '@/features/devtools/login-hint'; const I18N_KEY_PAGE_PREFIX = AUTH_SIGNUP_ENABLED @@ -36,7 +37,7 @@ export default function PageLoginVerify({ search: { redirect?: string; email: string }; }) { const { t } = useTranslation(['auth', 'common']); - const session = authClient.useSession(); + const session = useSession(); const form = useForm({ mode: 'onSubmit', diff --git a/src/features/auth/page-logout.tsx b/src/features/auth/page-logout.tsx index 87da0f442..4cd883493 100644 --- a/src/features/auth/page-logout.tsx +++ b/src/features/auth/page-logout.tsx @@ -6,10 +6,11 @@ import { PageError } from '@/components/errors/page-error'; import { Spinner } from '@/components/ui/spinner'; import { authClient } from '@/features/auth/client'; +import { useSession } from '@/features/auth/use-session'; export const PageLogout = () => { const navigate = useNavigate(); - const session = authClient.useSession(); + const session = useSession(); const { mutate, error } = useMutation({ mutationFn: async () => { const response = await authClient.signOut(); @@ -20,7 +21,7 @@ export const PageLogout = () => { }, onSuccess: () => { navigate({ - to: '/', + to: '/login', }); }, }); diff --git a/src/features/auth/page-onboarding.tsx b/src/features/auth/page-onboarding.tsx index 1f6921a9d..cd21b8a6c 100644 --- a/src/features/auth/page-onboarding.tsx +++ b/src/features/auth/page-onboarding.tsx @@ -15,15 +15,15 @@ import { } from '@/components/form'; import { Button } from '@/components/ui/button'; -import { authClient } from '@/features/auth/client'; import { ConfirmSignOut } from '@/features/auth/confirm-signout'; import { LayoutLogin } from '@/features/auth/layout-login'; import { useMascot } from '@/features/auth/mascot'; import { zFormFieldsOnboarding } from '@/features/auth/schema'; +import { useSession } from '@/features/auth/use-session'; export const PageOnboarding = () => { const { t } = useTranslation(['auth']); - const session = authClient.useSession(); + const session = useSession(); const submitOnboarding = useMutation( orpc.account.submitOnboarding.mutationOptions({ diff --git a/src/features/auth/session.server.ts b/src/features/auth/session.server.ts new file mode 100644 index 000000000..3f0087e5b --- /dev/null +++ b/src/features/auth/session.server.ts @@ -0,0 +1,20 @@ +import { getRequestHeaders } from '@tanstack/react-start/server'; + +import { auth } from '@/server/auth'; + +/** + * Server-only helper that pre-fetches the current session so the first paint + * after a refresh can skip the central spinner. Called from the root route + * `beforeLoad` during SSR and exposed through the route context. + */ +export type AuthSession = Awaited>; + +export const getAuthSession = async () => { + const session = await auth.api.getSession({ + headers: getRequestHeaders(), + }); + + return { + session: session ?? null, + } as const; +}; diff --git a/src/features/auth/use-session.ts b/src/features/auth/use-session.ts new file mode 100644 index 000000000..7c4517236 --- /dev/null +++ b/src/features/auth/use-session.ts @@ -0,0 +1,26 @@ +import { authClient } from '@/features/auth/client'; +import { RootRouteApi } from '@/routes/__root'; + +/** + * Wraps `authClient.useSession()` and, while the client-side session hook is + * still pending on first mount, falls back to the session pre-fetched on the + * server in the root route `beforeLoad`. This prevents content shifts after a + * refresh for components that read session data directly (UserCard, NavUser, + * onboarding, etc.) — they get real data on the very first render. + * + * Drop-in replacement for `authClient.useSession()`. + */ +export const useSession = () => { + const session = authClient.useSession(); + const { authSession } = RootRouteApi.useRouteContext(); + + if (session.isPending && authSession?.session !== undefined) { + return { + ...session, + data: authSession.session satisfies typeof session.data, + isPending: false, + }; + } + + return session; +}; diff --git a/src/features/auth/utils.ts b/src/features/auth/utils.ts index 64b2803f2..aff3354d7 100644 --- a/src/features/auth/utils.ts +++ b/src/features/auth/utils.ts @@ -3,11 +3,12 @@ import { useEffect } from 'react'; import { authClient } from '@/features/auth/client'; import { Role } from '@/features/auth/permissions'; +import { useSession } from '@/features/auth/use-session'; export const useRedirectAfterLogin = () => { const search = useSearch({ strict: false }); const router = useRouter(); - const session = authClient.useSession(); + const session = useSession(); const searchRedirect = search.redirect; useEffect(() => { diff --git a/src/features/auth/with-permissions.tsx b/src/features/auth/with-permissions.tsx index b95da398b..b851739d4 100644 --- a/src/features/auth/with-permissions.tsx +++ b/src/features/auth/with-permissions.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { authClient } from '@/features/auth/client'; import { Permission, Role } from '@/features/auth/permissions'; +import { useSession } from '@/features/auth/use-session'; export const WithPermissions = (props: { permissions: Permission[]; @@ -9,7 +10,7 @@ export const WithPermissions = (props: { loadingFallback?: ReactNode; fallback?: ReactNode; }) => { - const session = authClient.useSession(); + const session = useSession(); const userRole = session.data?.user.role; if (session.isPending) { diff --git a/src/features/demo/demo-app-switch.tsx b/src/features/demo/demo-app-switch.tsx index 11d8f5e8d..94d5f75ca 100644 --- a/src/features/demo/demo-app-switch.tsx +++ b/src/features/demo/demo-app-switch.tsx @@ -17,10 +17,11 @@ import { import { authClient } from '@/features/auth/client'; import { Role } from '@/features/auth/permissions'; +import { useSession } from '@/features/auth/use-session'; export const DemoAppSwitch = () => { const { t } = useTranslation(['demo']); - const session = authClient.useSession(); + const session = useSession(); const userRole = session.data?.user.role; const matchRoute = useMatchRoute(); const currentApp = matchRoute({ to: '/manager', fuzzy: true }) diff --git a/src/features/user/manager/form-user.tsx b/src/features/user/manager/form-user.tsx index e197bfbb0..302144dc8 100644 --- a/src/features/user/manager/form-user.tsx +++ b/src/features/user/manager/form-user.tsx @@ -8,13 +8,13 @@ import { FormFieldLabel, } from '@/components/form'; -import { authClient } from '@/features/auth/client'; import { rolesNames } from '@/features/auth/permissions'; +import { useSession } from '@/features/auth/use-session'; import { FormFieldsUser } from '@/features/user/schema'; export const FormUser = (props: { userId?: string }) => { const { t } = useTranslation(['user']); - const session = authClient.useSession(); + const session = useSession(); const form = useFormContext(); const isCurrentUser = props.userId === session.data?.user.id; diff --git a/src/features/user/manager/page-user-update.tsx b/src/features/user/manager/page-user-update.tsx index d25f4af7d..7be2048d4 100644 --- a/src/features/user/manager/page-user-update.tsx +++ b/src/features/user/manager/page-user-update.tsx @@ -17,7 +17,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; -import { authClient } from '@/features/auth/client'; +import { useSession } from '@/features/auth/use-session'; import { FormUser } from '@/features/user/manager/form-user'; import { zFormFieldsUser } from '@/features/user/schema'; import { @@ -30,7 +30,7 @@ import { export const PageUserUpdate = (props: { params: { id: string } }) => { const { t } = useTranslation(['user']); const { navigateBack } = useNavigateBack(); - const session = authClient.useSession(); + const session = useSession(); const queryClient = useQueryClient(); const userQuery = useQuery( orpc.user.getById.queryOptions({ input: { id: props.params.id } }) diff --git a/src/features/user/manager/page-user.tsx b/src/features/user/manager/page-user.tsx index ccf2a138a..4b00cd47f 100644 --- a/src/features/user/manager/page-user.tsx +++ b/src/features/user/manager/page-user.tsx @@ -41,7 +41,7 @@ import { ResponsiveIconButton } from '@/components/ui/responsive-icon-button'; import { Skeleton } from '@/components/ui/skeleton'; import { Spinner } from '@/components/ui/spinner'; -import { authClient } from '@/features/auth/client'; +import { useSession } from '@/features/auth/use-session'; import { WithPermissions } from '@/features/auth/with-permissions'; import { PageLayout, @@ -53,7 +53,7 @@ import { export const PageUser = (props: { params: { id: string } }) => { const queryClient = useQueryClient(); const { navigateBack } = useNavigateBack(); - const session = authClient.useSession(); + const session = useSession(); const { t } = useTranslation(['user']); const userQuery = useQuery( orpc.user.getById.queryOptions({ @@ -353,7 +353,7 @@ const UserSessions = (props: { userId: string }) => { const RevokeAllSessionsButton = (props: { userId: string }) => { const queryClient = useQueryClient(); - const currentSession = authClient.useSession(); + const currentSession = useSession(); const { t } = useTranslation(['user']); const revokeAllSessions = useMutation( orpc.user.revokeUserSessions.mutationOptions({ @@ -393,7 +393,7 @@ const RevokeSessionButton = (props: { sessionToken: string; }) => { const queryClient = useQueryClient(); - const currentSession = authClient.useSession(); + const currentSession = useSession(); const { t } = useTranslation(['user']); const revokeSession = useMutation( orpc.user.revokeUserSession.mutationOptions({ diff --git a/src/layout/manager/nav-user.tsx b/src/layout/manager/nav-user.tsx index 221b629df..6ff2320c8 100644 --- a/src/layout/manager/nav-user.tsx +++ b/src/layout/manager/nav-user.tsx @@ -33,8 +33,8 @@ import { } from '@/components/ui/sidebar'; import { themes } from '@/components/ui/theme-switcher'; -import { authClient } from '@/features/auth/client'; import { ConfirmSignOut } from '@/features/auth/confirm-signout'; +import { useSession } from '@/features/auth/use-session'; import { WithPermissions } from '@/features/auth/with-permissions'; import { BuildInfoDrawer } from '@/features/build-info/build-info-drawer'; import { BuildInfoVersion } from '@/features/build-info/build-info-version'; @@ -42,7 +42,7 @@ import { BuildInfoVersion } from '@/features/build-info/build-info-version'; export function NavUser() { const { t } = useTranslation(['common', 'auth', 'layout']); const { isMobile } = useSidebar(); - const session = authClient.useSession(); + const session = useSession(); const { setOpenMobile } = useSidebar(); const { theme, setTheme } = useTheme(); @@ -111,12 +111,15 @@ export function NavUser() { - + {themes.map((item) => ( ( )) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index d7ffe98bd..f367a0bb2 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -8,6 +8,7 @@ import { Outlet, Scripts, } from '@tanstack/react-router'; +import { getRouteApi } from '@tanstack/react-router'; import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; import { createServerFn } from '@tanstack/react-start'; import { type ReactNode } from 'react'; @@ -20,6 +21,8 @@ import { AVAILABLE_LANGUAGES } from '@/lib/i18n/constants'; import { PageError } from '@/components/errors/page-error'; import { MailDevDevtoolPanel } from '@/devtools/maildev'; +import type { AuthSession } from '@/features/auth/session.server'; +import { getAuthSession } from '@/features/auth/session.server'; import { EnvHint } from '@/features/devtools/env-hint'; import { Providers } from '@/providers'; import { getUserLanguage } from '@/server/utils'; @@ -34,6 +37,13 @@ const initSsrApp = createServerFn({ method: 'GET' }).handler(() => { export const Route = createRootRouteWithContext<{ queryClient: QueryClient; }>()({ + beforeLoad: async (): Promise<{ authSession: AuthSession | null }> => { + if (!import.meta.env.SSR) { + return { authSession: null }; + } + const authSession = await getAuthSession(); + return { authSession }; + }, loader: async () => { // Setup language and theme in SSR to prevent hydratation errors if (import.meta.env.SSR) { @@ -98,6 +108,8 @@ export const Route = createRootRouteWithContext<{ }), }); +export const RootRouteApi = getRouteApi('__root__'); + function RootComponent() { return ( @@ -143,7 +155,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) { : undefined, }} > - + diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 5a3a55b75..90b57a70d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,9 +1,12 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; +import { getAuthSession } from '@/features/auth/session.server'; + export const Route = createFileRoute('/')({ component: RouteComponent, - beforeLoad: () => { - throw redirect({ to: '/login' }); + beforeLoad: async () => { + const authSession = await getAuthSession(); + throw redirect({ to: authSession?.session ? '/app' : '/login' }); }, }); From 95f13a76395485abbe212b10ff453a286ca142c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Pe=CC=81rard?= Date: Sat, 25 Apr 2026 09:05:20 +0200 Subject: [PATCH 2/6] fix useSession condition --- src/features/auth/use-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/auth/use-session.ts b/src/features/auth/use-session.ts index 7c4517236..148ba5974 100644 --- a/src/features/auth/use-session.ts +++ b/src/features/auth/use-session.ts @@ -14,7 +14,7 @@ export const useSession = () => { const session = authClient.useSession(); const { authSession } = RootRouteApi.useRouteContext(); - if (session.isPending && authSession?.session !== undefined) { + if (session.isPending && authSession?.session) { return { ...session, data: authSession.session satisfies typeof session.data, From 72269718b45f62a7324d51ef0fd40f627de2fdef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Pe=CC=81rard?= Date: Sat, 25 Apr 2026 09:05:46 +0200 Subject: [PATCH 3/6] fix index redirect manage permissions --- src/routes/index.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 90b57a70d..0e6d25590 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,12 +1,30 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; +import { authClient } from '@/features/auth/client'; +import { Role } from '@/features/auth/permissions'; import { getAuthSession } from '@/features/auth/session.server'; export const Route = createFileRoute('/')({ component: RouteComponent, beforeLoad: async () => { const authSession = await getAuthSession(); - throw redirect({ to: authSession?.session ? '/app' : '/login' }); + + if (!authSession?.session) { + throw redirect({ to: '/login' }); + } + + if ( + authClient.admin.checkRolePermission({ + role: authSession.session.user.role as Role, + permissions: { + apps: ['manager'], + }, + }) + ) { + throw redirect({ to: '/manager' }); + } + + throw redirect({ to: '/app' }); }, }); From 3a382955a43db804634fb5c03cdaea695d51a039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Pe=CC=81rard?= Date: Sat, 25 Apr 2026 09:30:19 +0200 Subject: [PATCH 4/6] fix duplicated import --- src/routes/__root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index f367a0bb2..14e622840 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -4,11 +4,11 @@ import { QueryClient } from '@tanstack/react-query'; import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'; import { createRootRouteWithContext, + getRouteApi, HeadContent, Outlet, Scripts, } from '@tanstack/react-router'; -import { getRouteApi } from '@tanstack/react-router'; import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; import { createServerFn } from '@tanstack/react-start'; import { type ReactNode } from 'react'; From 5cc2d94436a2b1234415b3b6d1935ad2e6a0ba42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Pe=CC=81rard?= Date: Sat, 25 Apr 2026 09:33:44 +0200 Subject: [PATCH 5/6] fix build --- src/features/auth/session.server.ts | 20 -------------------- src/features/auth/session.ts | 20 ++++++++++++++++++++ src/features/auth/use-session.ts | 2 +- src/routes/__root.tsx | 5 ++--- src/routes/index.tsx | 6 +++--- 5 files changed, 26 insertions(+), 27 deletions(-) delete mode 100644 src/features/auth/session.server.ts create mode 100644 src/features/auth/session.ts diff --git a/src/features/auth/session.server.ts b/src/features/auth/session.server.ts deleted file mode 100644 index 3f0087e5b..000000000 --- a/src/features/auth/session.server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getRequestHeaders } from '@tanstack/react-start/server'; - -import { auth } from '@/server/auth'; - -/** - * Server-only helper that pre-fetches the current session so the first paint - * after a refresh can skip the central spinner. Called from the root route - * `beforeLoad` during SSR and exposed through the route context. - */ -export type AuthSession = Awaited>; - -export const getAuthSession = async () => { - const session = await auth.api.getSession({ - headers: getRequestHeaders(), - }); - - return { - session: session ?? null, - } as const; -}; diff --git a/src/features/auth/session.ts b/src/features/auth/session.ts new file mode 100644 index 000000000..fce6b6bec --- /dev/null +++ b/src/features/auth/session.ts @@ -0,0 +1,20 @@ +import { createServerFn } from '@tanstack/react-start'; +import { getRequestHeaders } from '@tanstack/react-start/server'; + +import { auth } from '@/server/auth'; + +export const initAuthSsr = createServerFn({ method: 'GET' }).handler( + async () => { + const authSession = await auth.api.getSession({ + headers: getRequestHeaders(), + }); + + return { + authSession, + }; + } +); + +export type AuthSession = Awaited< + ReturnType +>['authSession']; diff --git a/src/features/auth/use-session.ts b/src/features/auth/use-session.ts index 148ba5974..e87007aee 100644 --- a/src/features/auth/use-session.ts +++ b/src/features/auth/use-session.ts @@ -17,7 +17,7 @@ export const useSession = () => { if (session.isPending && authSession?.session) { return { ...session, - data: authSession.session satisfies typeof session.data, + data: authSession satisfies typeof session.data, isPending: false, }; } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 14e622840..cba1ae5ea 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -21,8 +21,7 @@ import { AVAILABLE_LANGUAGES } from '@/lib/i18n/constants'; import { PageError } from '@/components/errors/page-error'; import { MailDevDevtoolPanel } from '@/devtools/maildev'; -import type { AuthSession } from '@/features/auth/session.server'; -import { getAuthSession } from '@/features/auth/session.server'; +import { type AuthSession, initAuthSsr } from '@/features/auth/session'; import { EnvHint } from '@/features/devtools/env-hint'; import { Providers } from '@/providers'; import { getUserLanguage } from '@/server/utils'; @@ -41,7 +40,7 @@ export const Route = createRootRouteWithContext<{ if (!import.meta.env.SSR) { return { authSession: null }; } - const authSession = await getAuthSession(); + const { authSession } = await initAuthSsr(); return { authSession }; }, loader: async () => { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 0e6d25590..eb3a9d69b 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -2,12 +2,12 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; import { authClient } from '@/features/auth/client'; import { Role } from '@/features/auth/permissions'; -import { getAuthSession } from '@/features/auth/session.server'; +import { initAuthSsr } from '@/features/auth/session'; export const Route = createFileRoute('/')({ component: RouteComponent, beforeLoad: async () => { - const authSession = await getAuthSession(); + const { authSession } = await initAuthSsr(); if (!authSession?.session) { throw redirect({ to: '/login' }); @@ -15,7 +15,7 @@ export const Route = createFileRoute('/')({ if ( authClient.admin.checkRolePermission({ - role: authSession.session.user.role as Role, + role: authSession.user.role as Role, permissions: { apps: ['manager'], }, From e6db0cd9971d27958ea4d906f32089008e07a00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Pe=CC=81rard?= Date: Thu, 7 May 2026 09:59:37 +0200 Subject: [PATCH 6/6] fix e2e --- src/features/auth/guard-authenticated.tsx | 26 ++++++++++++++------- src/features/user/manager/page-user-new.tsx | 10 ++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/features/auth/guard-authenticated.tsx b/src/features/auth/guard-authenticated.tsx index 7dd2e6660..f3340d96c 100644 --- a/src/features/auth/guard-authenticated.tsx +++ b/src/features/auth/guard-authenticated.tsx @@ -1,5 +1,5 @@ import { useRouter } from '@tanstack/react-router'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; import { PageError } from '@/components/errors/page-error'; import { Spinner } from '@/components/ui/spinner'; @@ -19,6 +19,23 @@ export const GuardAuthenticated = ({ const session = useSession(); const router = useRouter(); + useEffect(() => { + if ( + session.isPending || + (session.error && session.error.status > 0) || + session.data?.user + ) + return; + + router.navigate({ + to: '/login', + replace: true, + search: { + redirect: location.href, + }, + }); + }, [router, session.isPending, session.data?.user, session.error]); + if (session.isPending) { return ; } @@ -28,13 +45,6 @@ export const GuardAuthenticated = ({ } if (!session.data?.user) { - router.navigate({ - to: '/login', - replace: true, - search: { - redirect: location.href, - }, - }); return null; } diff --git a/src/features/user/manager/page-user-new.tsx b/src/features/user/manager/page-user-new.tsx index 16727c9da..3c4f0e1fd 100644 --- a/src/features/user/manager/page-user-new.tsx +++ b/src/features/user/manager/page-user-new.tsx @@ -1,12 +1,12 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { ORPCError } from '@orpc/client'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useRouter } from '@tanstack/react-router'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { orpc } from '@/lib/orpc/client'; -import { useNavigateBack } from '@/hooks/use-navigate-back'; import { BackButton } from '@/components/back-button'; import { Form } from '@/components/form'; @@ -25,7 +25,7 @@ import { export const PageUserNew = () => { const { t } = useTranslation(['user']); - const { navigateBack } = useNavigateBack(); + const router = useRouter(); const queryClient = useQueryClient(); const form = useForm({ resolver: zodResolver(zFormFieldsUser()), @@ -45,8 +45,10 @@ export const PageUserNew = () => { type: 'all', }); - // Redirect - navigateBack({ ignoreBlocker: true }); + await router.navigate({ + to: '/manager/users', + ignoreBlocker: true, + }); }, onError: (error) => { if (