From 1f9f9194434ff3dac13c6dcd8607e551cbcac949 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Wed, 19 Aug 2026 19:48:24 -0400 Subject: [PATCH] feat(auth): add return-URL flow for login redirects Add LoginLink client component that captures current path via usePathname() and passes it as returnTo query param. Update login page to read and sanitize returnTo (open-redirect guard: must start with /, reject //). Update LoginForm to redirect to returnTo after successful login. Wire LandingPage CTA through LoginLink. Add returnTo redirect for already- authed users hitting /login. Add return-URL to flappy page redirect. Remove trailing blank line from (frontend) layout. --- src/app/(frontend)/flappy/page.tsx | 3 ++- src/app/(frontend)/layout.tsx | 1 - src/app/login/page.tsx | 18 +++++++++++++++--- src/components/frontend/LandingPage.tsx | 4 ++-- src/components/frontend/auth/LoginForm.tsx | 13 +++++++++++-- src/components/frontend/auth/LoginLink.tsx | 12 ++++++++++++ 6 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 src/components/frontend/auth/LoginLink.tsx diff --git a/src/app/(frontend)/flappy/page.tsx b/src/app/(frontend)/flappy/page.tsx index bf484f2..c191a76 100644 --- a/src/app/(frontend)/flappy/page.tsx +++ b/src/app/(frontend)/flappy/page.tsx @@ -18,7 +18,8 @@ export default async function FlappyPage() { const { user } = await payload.auth({ headers, canSetHeaders: false }); if (!user) { - redirect("/login"); + const pathname = headers.get("x-invoke-path") || "/flappy"; + redirect(`/login?returnTo=${encodeURIComponent(pathname)}`); } const profileRes = await payload.find({ diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 5bf69a7..239c7cf 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -40,7 +40,6 @@ export const metadata: Metadata = { export default async function RootLayout(props: { children: React.ReactNode }) { const { children } = props; - const headers = await nextHeaders(); const payloadConfig = await config; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4cfa177..37076a7 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -10,12 +10,24 @@ import AppLogo from "@/components/graphics/AppLogo"; // (no DB is available at build time in a container). export const dynamic = "force-dynamic"; -export default async function LoginPage() { +function safeReturnTo(value: string | undefined): string | undefined { + if (!value?.startsWith("/") || value.startsWith("//")) return undefined; + return value; +} + +export default async function LoginPage({ + searchParams, +}: { + searchParams: Promise<{ returnTo?: string }>; +}) { + const { returnTo } = await searchParams; + const target = safeReturnTo(returnTo); + const payload = await getPayload({ config }); const headers = await nextHeaders(); const { user } = await payload.auth({ headers, canSetHeaders: false }); - if (user) redirect("/"); + if (user) redirect(target ?? "/"); return (
@@ -27,7 +39,7 @@ export default async function LoginPage() { Log in to access the operations dashboard, intelligence, and logistics.

- + ); } diff --git a/src/components/frontend/LandingPage.tsx b/src/components/frontend/LandingPage.tsx index 425d81b..d812718 100644 --- a/src/components/frontend/LandingPage.tsx +++ b/src/components/frontend/LandingPage.tsx @@ -1,6 +1,6 @@ -import Link from "next/link"; import { Button } from "@/components/ui/button"; import AppLogo from "@/components/graphics/AppLogo"; +import { LoginLink } from "@/components/frontend/auth/LoginLink"; export function LandingPage() { return ( @@ -14,7 +14,7 @@ export function LandingPage() { operations dashboard, intelligence, and logistics.

diff --git a/src/components/frontend/auth/LoginForm.tsx b/src/components/frontend/auth/LoginForm.tsx index 1c782db..ee8ef28 100644 --- a/src/components/frontend/auth/LoginForm.tsx +++ b/src/components/frontend/auth/LoginForm.tsx @@ -7,7 +7,11 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -export function LoginForm() { +interface LoginFormProps { + returnTo?: string; +} + +export function LoginForm({ returnTo }: LoginFormProps) { const router = useRouter(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -33,7 +37,12 @@ export function LoginForm() { return; } - router.push("/"); + // Redirect to returnTo if provided, otherwise to home + if (returnTo && returnTo !== "/") { + router.push(returnTo); + } else { + router.push("/"); + } router.refresh(); } catch { setError("Something went wrong. Please try again."); diff --git a/src/components/frontend/auth/LoginLink.tsx b/src/components/frontend/auth/LoginLink.tsx new file mode 100644 index 0000000..94d041c --- /dev/null +++ b/src/components/frontend/auth/LoginLink.tsx @@ -0,0 +1,12 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import type { ComponentProps } from "react"; + +type LoginLinkProps = Omit, "href">; + +export function LoginLink(props: LoginLinkProps) { + const pathname = usePathname(); + return ; +}