TanStackT
TanStackโ€ข5d agoโ€ข
9 replies
endless-jade

Correct way to protect routes on server and client with better auth

Hi all ๐Ÿ‘‹

Currently integrating a Tanstack Start + React query app with Better auth.

This is my setup at the moment:

src/features/auth/fn/get-user.ts
import { createServerFn } from "@tanstack/react-start";
import { getRequest, setResponseHeader } from "@tanstack/react-start/server";

import { auth } from "@/lib/auth";

export const getUser = createServerFn({ method: "GET" }).handler(async () => {
    const session = await auth.api.getSession({
        headers: getRequest().headers,
        returnHeaders: true,
    });

    // Forward any Set-Cookie headers to the client, e.g. for session/cache refresh
    const cookies = session.headers?.getSetCookie();
    if (cookies?.length) {
        setResponseHeader("Set-Cookie", cookies);
    }

    return session.response?.user || null;
});


query-options.ts:
import { queryOptions } from "@tanstack/react-query";

import { getUser } from "../fn/get-user";
import { AUTH_KEYS } from "./query-keys";

export const authQueryOptions = () =>
    queryOptions({
        queryKey: AUTH_KEYS.user,
        queryFn: ({ signal }) => getUser({ signal }),
        staleTime: 1000 * 60 * 5, // 5 minutes - auth data doesn't change often
        gcTime: 1000 * 60 * 10, // 10 minutes in cache
    });

export type AuthQueryResult = Awaited<ReturnType<typeof getUser>>;


src/middleware/route-auth.ts
export const authMiddleware = createMiddleware().server(async ({ next }) => {
    const headers = getRequestHeaders();
    const session = await auth.api.getSession({ headers });

    if (!session) {
        throw redirect({ to: "/login" });
    }

    return await next({
        context: {
            headers,
            session,
            user: session.user,
        },
    });
});


src/routes/_protected/route.tsx
export const Route = createFileRoute("/_protected")({
    component: PlatformLayout,
    server: {
            middleware: [authMiddleware],
    },
    beforeLoad: async ({ context }) => {
        const user = await context.queryClient.ensureQueryData({
            ...authQueryOptions(),
            revalidateIfStale: true,
        });
        if (!user) {
            throw redirect({ to: "/login" });
        }

        // re-return to update type as non-null for child routes
        return { user };
    },
});


Guest routes protection:
// For routes like /login, /signup
beforeLoad: async ({ context }) => {
  const user = await context.queryClient.ensureQueryData({
    ...authQueryOptions(),
    revalidateIfStale: true,
  });
  if (user) {
    throw redirect({ to: "/dashboard" }); // redirect authenticated users away
  }
},


useAuth hook:
export const useAuth = () => {
  const { data: user, isPending } = useQuery(authQueryOptions());
  return { user, isPending };
};

// Usage in components
const { user } = useAuth();


I obviously still have my server.middleware set to [authMiddleware] in the Route, so if it happens on the server, it's protected.
Then the
beforeLoad
stuff protects on the client side.

Is this the correct/recommended way tho? Am I missing or overcomplicating something? ๐Ÿค”
Was this page helpful?