From d9c847c632e54bee390d2cbb746b65ba5200d292 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:38:44 +0000 Subject: [PATCH] fix: resolve Next.js build error with useSearchParams in LoginForm - Update login page to be server component that extracts searchParams - Pass redirectTo as prop to LoginForm instead of using useSearchParams - Add helper function to reduce code duplication in middleware - Add URL validation in layout component as suggested in PR comments - Follow Next.js App Router best practices for server/client components Co-Authored-By: nate@buster.so --- apps/web/src/app/app/layout.tsx | 11 ++++++++++- apps/web/src/app/auth/login/page.tsx | 11 +++++++++-- .../src/components/features/auth/LoginForm.tsx | 5 +---- apps/web/src/middleware/assetReroutes.ts | 17 +++++++++-------- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/app/layout.tsx b/apps/web/src/app/app/layout.tsx index dcb887c9c..a6f4fd0cb 100644 --- a/apps/web/src/app/app/layout.tsx +++ b/apps/web/src/app/app/layout.tsx @@ -10,6 +10,15 @@ import { createBusterRoute } from '@/routes'; import { BusterRoutes } from '@/routes/busterRoutes'; import { ClientRedirect } from '../../components/ui/layouts/ClientRedirect'; +const isValidRedirectUrl = (url: string): boolean => { + try { + const decoded = decodeURIComponent(url); + return decoded.startsWith('/') && !decoded.startsWith('//'); + } catch { + return false; + } +}; + const newUserRoute = createBusterRoute({ route: BusterRoutes.NEW_USER }); const loginRoute = createBusterRoute({ route: BusterRoutes.AUTH_LOGIN }); @@ -40,7 +49,7 @@ export default async function Layout({ (supabaseContext.user?.is_anonymous && pathname !== loginRoute) || !supabaseContext?.user?.id ) { - const redirectParam = pathname ? encodeURIComponent(pathname) : ''; + const redirectParam = pathname && isValidRedirectUrl(pathname) ? encodeURIComponent(pathname) : ''; const loginUrlWithRedirect = redirectParam ? `${loginRoute}?next=${redirectParam}` : loginRoute; return ; } diff --git a/apps/web/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx index 72a2fa9a3..9a9a0f236 100644 --- a/apps/web/src/app/auth/login/page.tsx +++ b/apps/web/src/app/auth/login/page.tsx @@ -1,5 +1,12 @@ import { LoginForm } from '@/components/features/auth/LoginForm'; -export default function Login() { - return ; +export default async function Login({ + searchParams +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }> +}) { + const params = await searchParams; + const redirectTo = typeof params.next === 'string' ? params.next : null; + + return ; } diff --git a/apps/web/src/components/features/auth/LoginForm.tsx b/apps/web/src/components/features/auth/LoginForm.tsx index ec4321eb3..f61f1e86c 100644 --- a/apps/web/src/components/features/auth/LoginForm.tsx +++ b/apps/web/src/components/features/auth/LoginForm.tsx @@ -2,7 +2,6 @@ import Cookies from 'js-cookie'; import Link from 'next/link'; -import { useSearchParams } from 'next/navigation'; import React, { useMemo, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { Button } from '@/components/ui/buttons'; @@ -31,9 +30,7 @@ const DEFAULT_CREDENTIALS = { password: process.env.NEXT_PUBLIC_USER_PASSWORD || '' }; -export const LoginForm: React.FC = () => { - const searchParams = useSearchParams(); - const redirectTo = searchParams.get('next'); +export const LoginForm: React.FC<{ redirectTo?: string | null }> = ({ redirectTo }) => { const [loading, setLoading] = useState<'google' | 'github' | 'azure' | 'email' | null>(null); const [errorMessages, setErrorMessages] = useState([]); const [signUpFlow, setSignUpFlow] = useState(true); diff --git a/apps/web/src/middleware/assetReroutes.ts b/apps/web/src/middleware/assetReroutes.ts index 5bb39a8dc..056fb1ba4 100644 --- a/apps/web/src/middleware/assetReroutes.ts +++ b/apps/web/src/middleware/assetReroutes.ts @@ -8,6 +8,13 @@ import { isShareableAssetPage } from '@/routes/busterRoutes'; +const createLoginRedirect = (request: NextRequest): NextResponse => { + const originalUrl = `${request.nextUrl.pathname}${request.nextUrl.search}`; + const loginUrl = new URL(createBusterRoute({ route: BusterRoutes.AUTH_LOGIN }), request.url); + loginUrl.searchParams.set('next', encodeURIComponent(originalUrl)); + return NextResponse.redirect(loginUrl); +}; + export const assetReroutes = async ( request: NextRequest, response: NextResponse, @@ -15,10 +22,7 @@ export const assetReroutes = async ( ) => { const userExists = !!user && !!user.id; if (!userExists && !isPublicPage(request)) { - const originalUrl = `${request.nextUrl.pathname}${request.nextUrl.search}`; - const loginUrl = new URL(createBusterRoute({ route: BusterRoutes.AUTH_LOGIN }), request.url); - loginUrl.searchParams.set('next', encodeURIComponent(originalUrl)); - return NextResponse.redirect(loginUrl); + return createLoginRedirect(request); } if (!userExists && isShareableAssetPage(request)) { @@ -26,10 +30,7 @@ export const assetReroutes = async ( if (redirect) { return NextResponse.redirect(new URL(redirect, request.url)); } - const originalUrl = `${request.nextUrl.pathname}${request.nextUrl.search}`; - const loginUrl = new URL(createBusterRoute({ route: BusterRoutes.AUTH_LOGIN }), request.url); - loginUrl.searchParams.set('next', encodeURIComponent(originalUrl)); - return NextResponse.redirect(loginUrl); + return createLoginRedirect(request); } return response;