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:
parent
eef1b11bb1
commit
1f9f919443
6 changed files with 42 additions and 9 deletions
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push("/");
|
// Redirect to returnTo if provided, otherwise to home
|
||||||
|
if (returnTo && returnTo !== "/") {
|
||||||
|
router.push(returnTo);
|
||||||
|
} else {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch {
|
} catch {
|
||||||
setError("Something went wrong. Please try again.");
|
setError("Something went wrong. Please try again.");
|
||||||
|
|
|
||||||
12
src/components/frontend/auth/LoginLink.tsx
Normal file
12
src/components/frontend/auth/LoginLink.tsx
Normal 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)}`} />;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue