From e9a784e88104c682c382b644c0949da356b26fd3 Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Tue, 18 Aug 2026 14:29:50 +0200 Subject: [PATCH 1/3] Preserve requested URL through Google sign-in --- services/hackbot-ui/app/login/page.tsx | 30 ++++++++++++++++++++++++-- services/hackbot-ui/middleware.ts | 5 +++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/services/hackbot-ui/app/login/page.tsx b/services/hackbot-ui/app/login/page.tsx index 1a526a5a14..7af0a6bb19 100644 --- a/services/hackbot-ui/app/login/page.tsx +++ b/services/hackbot-ui/app/login/page.tsx @@ -4,6 +4,26 @@ import { useState } from "react"; import { signIn } from "@/lib/auth-client"; +const DEFAULT_PATH = "/"; + +function safeRedirectPath(raw: string | null): string { + if ( + !raw || + !raw.startsWith("/") || + raw.startsWith("//") || + raw.includes("\\") + ) { + return DEFAULT_PATH; + } + + const path = raw.split(/[?#]/)[0]; + if (path === "/login" || path.startsWith("/login/")) { + return DEFAULT_PATH; + } + + return raw; +} + export default function LoginPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -11,11 +31,17 @@ export default function LoginPage() { async function onGoogle() { setError(null); setLoading(true); + const params = new URLSearchParams(window.location.search); + const next = safeRedirectPath(params.get("next")); + + // Keep the target across a denied sign-in so a retry still lands on it. + const errorParams = new URLSearchParams({ error: "denied", next }); + try { await signIn.social({ provider: "google", - callbackURL: "/", - errorCallbackURL: "/login?error=denied", + callbackURL: next, + errorCallbackURL: `/login?${errorParams.toString()}`, }); } catch (err) { setError((err as Error).message); diff --git a/services/hackbot-ui/middleware.ts b/services/hackbot-ui/middleware.ts index 11cfecc912..8fdb6c0c6b 100644 --- a/services/hackbot-ui/middleware.ts +++ b/services/hackbot-ui/middleware.ts @@ -16,7 +16,12 @@ export function middleware(req: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + // Remember the requested URL so the login page can send the user back here const loginUrl = new URL("/login", req.url); + loginUrl.searchParams.set( + "next", + `${req.nextUrl.pathname}${req.nextUrl.search}` + ); return NextResponse.redirect(loginUrl); } From d24f940f291091cc3ea2b66d7022c4d41793d901 Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Tue, 18 Aug 2026 15:02:19 +0200 Subject: [PATCH 2/3] Reject control characters in login redirects --- services/hackbot-ui/app/login/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/hackbot-ui/app/login/page.tsx b/services/hackbot-ui/app/login/page.tsx index 7af0a6bb19..28a41ce5c8 100644 --- a/services/hackbot-ui/app/login/page.tsx +++ b/services/hackbot-ui/app/login/page.tsx @@ -5,13 +5,16 @@ import { useState } from "react"; import { signIn } from "@/lib/auth-client"; const DEFAULT_PATH = "/"; +// Browsers may strip these characters and change how the URL is parsed. +const CONTROL_CHARS = /[\u0000-\u001f\u007f]/; function safeRedirectPath(raw: string | null): string { if ( !raw || !raw.startsWith("/") || raw.startsWith("//") || - raw.includes("\\") + raw.includes("\\") || + CONTROL_CHARS.test(raw) ) { return DEFAULT_PATH; } From 851c8676dbae8eda10aa333d0baa1f9039dc883e Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Thu, 20 Aug 2026 17:34:27 +0200 Subject: [PATCH 3/3] "Fix login callback redirect " --- services/hackbot-ui/app/login/page.tsx | 47 ++++++++------------------ services/hackbot-ui/middleware.ts | 5 +-- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/services/hackbot-ui/app/login/page.tsx b/services/hackbot-ui/app/login/page.tsx index 28a41ce5c8..1a8d905db2 100644 --- a/services/hackbot-ui/app/login/page.tsx +++ b/services/hackbot-ui/app/login/page.tsx @@ -1,49 +1,34 @@ "use client"; -import { useState } from "react"; +import { use, useState } from "react"; import { signIn } from "@/lib/auth-client"; -const DEFAULT_PATH = "/"; -// Browsers may strip these characters and change how the URL is parsed. -const CONTROL_CHARS = /[\u0000-\u001f\u007f]/; - -function safeRedirectPath(raw: string | null): string { - if ( - !raw || - !raw.startsWith("/") || - raw.startsWith("//") || - raw.includes("\\") || - CONTROL_CHARS.test(raw) - ) { - return DEFAULT_PATH; - } - - const path = raw.split(/[?#]/)[0]; - if (path === "/login" || path.startsWith("/login/")) { - return DEFAULT_PATH; - } - - return raw; -} - -export default function LoginPage() { +type LoginPageProps = { + searchParams: Promise<{ + callbackURL?: string | string[]; + error?: string | string[]; + }>; +}; + +export default function LoginPage({ searchParams }: LoginPageProps) { + const params = use(searchParams); + const callbackURL = + typeof params.callbackURL === "string" ? params.callbackURL : "/"; const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function onGoogle() { setError(null); setLoading(true); - const params = new URLSearchParams(window.location.search); - const next = safeRedirectPath(params.get("next")); // Keep the target across a denied sign-in so a retry still lands on it. - const errorParams = new URLSearchParams({ error: "denied", next }); + const errorParams = new URLSearchParams({ error: "denied", callbackURL }); try { await signIn.social({ provider: "google", - callbackURL: next, + callbackURL, errorCallbackURL: `/login?${errorParams.toString()}`, }); } catch (err) { @@ -52,9 +37,7 @@ export default function LoginPage() { } } - const denied = - typeof window !== "undefined" && - new URLSearchParams(window.location.search).get("error"); + const denied = params.error === "denied"; return (
diff --git a/services/hackbot-ui/middleware.ts b/services/hackbot-ui/middleware.ts index 8fdb6c0c6b..3310836f9f 100644 --- a/services/hackbot-ui/middleware.ts +++ b/services/hackbot-ui/middleware.ts @@ -18,10 +18,7 @@ export function middleware(req: NextRequest) { // Remember the requested URL so the login page can send the user back here const loginUrl = new URL("/login", req.url); - loginUrl.searchParams.set( - "next", - `${req.nextUrl.pathname}${req.nextUrl.search}` - ); + loginUrl.searchParams.set("callbackURL", req.nextUrl.toString()); return NextResponse.redirect(loginUrl); }