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 ;
+}