1
0
Fork 0

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.
This commit is contained in:
Jason Fraley 2026-08-19 19:48:24 -04:00
parent eef1b11bb1
commit 1f9f919443
6 changed files with 42 additions and 9 deletions

View file

@ -18,7 +18,8 @@ export default async function FlappyPage() {
const { user } = await payload.auth({ headers, canSetHeaders: false }); const { user } = await payload.auth({ headers, canSetHeaders: false });
if (!user) { if (!user) {
redirect("/login"); const pathname = headers.get("x-invoke-path") || "/flappy";
redirect(`/login?returnTo=${encodeURIComponent(pathname)}`);
} }
const profileRes = await payload.find({ const profileRes = await payload.find({

View file

@ -40,7 +40,6 @@ export const metadata: Metadata = {
export default async function RootLayout(props: { children: React.ReactNode }) { export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props; const { children } = props;
const headers = await nextHeaders(); const headers = await nextHeaders();
const payloadConfig = await config; const payloadConfig = await config;

View file

@ -10,12 +10,24 @@ import AppLogo from "@/components/graphics/AppLogo";
// (no DB is available at build time in a container). // (no DB is available at build time in a container).
export const dynamic = "force-dynamic"; 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 payload = await getPayload({ config });
const headers = await nextHeaders(); const headers = await nextHeaders();
const { user } = await payload.auth({ headers, canSetHeaders: false }); const { user } = await payload.auth({ headers, canSetHeaders: false });
if (user) redirect("/"); if (user) redirect(target ?? "/");
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center p-5 gap-6 bg-background"> <div className="min-h-screen flex flex-col items-center justify-center p-5 gap-6 bg-background">
@ -27,7 +39,7 @@ export default async function LoginPage() {
Log in to access the operations dashboard, intelligence, and logistics. Log in to access the operations dashboard, intelligence, and logistics.
</p> </p>
</div> </div>
<LoginForm /> <LoginForm returnTo={target} />
</div> </div>
); );
} }

View file

@ -1,6 +1,6 @@
import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import AppLogo from "@/components/graphics/AppLogo"; import AppLogo from "@/components/graphics/AppLogo";
import { LoginLink } from "@/components/frontend/auth/LoginLink";
export function LandingPage() { export function LandingPage() {
return ( return (
@ -14,7 +14,7 @@ export function LandingPage() {
operations dashboard, intelligence, and logistics. operations dashboard, intelligence, and logistics.
</p> </p>
<Button asChild className="mt-2"> <Button asChild className="mt-2">
<Link href="/login">Log in to access the portal</Link> <LoginLink>Log in to access the portal</LoginLink>
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -7,7 +7,11 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
export function LoginForm() { interface LoginFormProps {
returnTo?: string;
}
export function LoginForm({ returnTo }: LoginFormProps) {
const router = useRouter(); const router = useRouter();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -33,7 +37,12 @@ export function LoginForm() {
return; return;
} }
// Redirect to returnTo if provided, otherwise to home
if (returnTo && returnTo !== "/") {
router.push(returnTo);
} else {
router.push("/"); router.push("/");
}
router.refresh(); router.refresh();
} catch { } catch {
setError("Something went wrong. Please try again."); setError("Something went wrong. Please try again.");

View file

@ -0,0 +1,12 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import type { ComponentProps } from "react";
type LoginLinkProps = Omit<ComponentProps<typeof Link>, "href">;
export function LoginLink(props: LoginLinkProps) {
const pathname = usePathname();
return <Link {...props} href={`/login?returnTo=${encodeURIComponent(pathname)}`} />;
}