Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
Expand All @@ -13,6 +13,7 @@ export default function SignInForm({
}: {
onSwitchToSignUp: () => void;
}) {
const search = useSearch({ from: "/login" });
const navigate = useNavigate({
from: "/",
});
Expand All @@ -32,7 +33,7 @@ export default function SignInForm({
{
onSuccess: () => {
navigate({
to: "/dashboard",
to: search.redirect,
});
Comment on lines 35 to 37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate redirect URL to prevent open redirects

Using search.redirect directly for navigation creates a security vulnerability. Malicious actors could craft URLs with arbitrary redirect destinations, leading to open redirect attacks.

Add validation to ensure the redirect URL is safe:

  onSuccess: () => {
+   const safeRedirect = search.redirect?.startsWith('/') ? search.redirect : '/dashboard';
    navigate({
-     to: search.redirect,
+     to: safeRedirect,
    });
    toast.success("Sign in successful");
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
navigate({
to: "/dashboard",
to: search.redirect,
});
onSuccess: () => {
const safeRedirect = search.redirect?.startsWith('/') ? search.redirect : '/dashboard';
navigate({
to: safeRedirect,
});
toast.success("Sign in successful");
},
🤖 Prompt for AI Agents
In
apps/cli/templates/auth/web/react/tanstack-router/src/components/sign-in-form.tsx
around lines 35 to 37, the code uses search.redirect directly for navigation,
which can cause open redirect vulnerabilities. To fix this, add validation to
check that the redirect URL is safe and allowed before calling navigate.
Implement a whitelist or ensure the redirect URL is relative and does not lead
to external sites, and only navigate if the URL passes this validation.

toast.success("Sign in successful");
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
Expand All @@ -13,6 +13,7 @@ export default function SignUpForm({
}: {
onSwitchToSignIn: () => void;
}) {
const search = useSearch({ from: "/login" });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same security concern as sign-in form

This component has the same open redirect vulnerability as the sign-in form. The search.redirect value should be validated before navigation.

Apply the same validation as suggested for the sign-in form:

  const search = useSearch({ from: "/login" });
  
  // ... later in onSuccess
  onSuccess: () => {
+   const safeRedirect = search.redirect?.startsWith('/') ? search.redirect : '/dashboard';
    navigate({
-     to: search.redirect,
+     to: safeRedirect,
    });
    toast.success("Sign up successful");
  },

Also applies to: 37-39

🤖 Prompt for AI Agents
In
apps/cli/templates/auth/web/react/tanstack-router/src/components/sign-up-form.tsx
at lines 16 and 37-39, the use of search.redirect for navigation introduces an
open redirect vulnerability. To fix this, validate the search.redirect value
against a whitelist of allowed paths or ensure it is a relative path within the
application before using it for navigation. Implement the same validation logic
as applied in the sign-in form to prevent unsafe redirects.

const navigate = useNavigate({
from: "/",
});
Expand All @@ -34,7 +35,7 @@ export default function SignUpForm({
{
onSuccess: () => {
navigate({
to: "/dashboard",
to: search.redirect,
});
toast.success("Sign up successful");
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";

export const Route = createFileRoute("/_auth")({
beforeLoad: async ({ context }) => {
if (!context.user) {
throw redirect({
to: "/login",
search: {
redirect: `${location.pathname}${location.search}${location.hash}`,
},
});
}
},
component: () => <Outlet />,
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,22 @@ import { trpc } from "@/utils/trpc";
import { useQuery } from "@tanstack/react-query";
{{/if}}
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";

export const Route = createFileRoute("/dashboard")({
export const Route = createFileRoute("/_auth/dashboard")({
component: RouteComponent,
});

function RouteComponent() {
const { data: session, isPending } = authClient.useSession();

const navigate = Route.useNavigate();

{{#if (eq api "orpc")}}

const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}

const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}

useEffect(() => {
if (!session && !isPending) {
navigate({
to: "/login",
});
}
}, [session, isPending]);

if (isPending) {
return <div>Loading...</div>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { useState } from "react";
import { z } from "zod";

const fallbackRedirect = "/dashboard";

export const Route = createFileRoute("/login")({
validateSearch: z.object({
redirect: z.string().default(fallbackRedirect),
}),
beforeLoad: async ({ context, search }) => {
if (context.user) {
throw redirect({ to: search.redirect });
}
},
component: RouteComponent,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const router = createRouter({
defaultPreload: "intent",
defaultPendingComponent: () => <Loader />,
{{#if (eq api "orpc")}}
context: { orpc, queryClient },
context: { orpc, queryClient{{#if auth}}, user: null{{/if}} },
Wrap: function WrapComponent({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
Expand All @@ -30,7 +30,7 @@ const router = createRouter({
);
},
{{else if (eq api "trpc")}}
context: { trpc, queryClient },
context: { trpc, queryClient{{#if auth}}, user: null{{/if}} },
Wrap: function WrapComponent({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
Expand All @@ -44,7 +44,9 @@ const router = createRouter({
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
},
{{else}}
context: {},
context: {
{{#if auth}}user: null{{/if}}
},
{{/if}}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import type { trpc } from "@/utils/trpc";
import type { QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
{{/if}}
{{#if auth}}
import { authClient } from "@/lib/auth-client";
import type { User } from "better-auth";
{{/if}}
import {
HeadContent,
Outlet,
Expand All @@ -30,17 +34,38 @@ import "../index.css";
export interface RouterAppContext {
orpc: typeof orpc;
queryClient: QueryClient;
{{#if auth}}
user: User | null;
{{/if}}
}
{{else if (eq api "trpc")}}
export interface RouterAppContext {
trpc: typeof trpc;
queryClient: QueryClient;
{{#if auth}}
user: User | null;
{{/if}}
}
{{else}}
export interface RouterAppContext {}
export interface RouterAppContext {
{{#if auth}}
user: User | null;
{{/if}}
}
{{/if}}

export const Route = createRootRouteWithContext<RouterAppContext>()({
{{#if auth}}
beforeLoad: async () => {
try {
const session = await authClient.getSession();
return { user: session.data?.user ?? null };
} catch (error) {
console.error('Failed to fetch user session:', error);
return { user: null };
}
},
{{/if}}
component: RootComponent,
head: () => ({
meta: [
Expand All @@ -65,8 +90,8 @@ function RootComponent() {
const isFetching = useRouterState({
select: (s) => s.isLoading,
});

{{#if (eq api "orpc")}}

const [client] = useState<RouterClient<typeof appRouter>>(() => createORPCClient(link));
const [orpcUtils] = useState(() => createTanstackQueryUtils(client));
{{/if}}
Expand Down