fix: preserve destination through the login redirect

Auth gates redirected to a bare /login with no memory of where the
user was headed, and the login page always sent them to /dashboard
afterward — so opening the /m PWA while signed out landed you on the
main dashboard instead of back on /m. Both gates now pass ?next=,
and login honors it (falling back to /dashboard if missing/unsafe).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-09-01 15:47:27 +02:00
parent 2014814c84
commit ac201cb0a1
5 changed files with 24 additions and 10 deletions

View file

@ -1,7 +1,7 @@
"use client";
import { useAuth } from "@/components/auth-provider";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useEffect } from "react";
import { Sidebar } from "@/components/sidebar";
import { BrandStripe } from "@/components/brand-stripe";
@ -9,12 +9,13 @@ import { BrandStripe } from "@/components/brand-stripe";
export default function AppLayout({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
if (!loading && !user) {
router.push("/login");
router.push(`/login?next=${encodeURIComponent(pathname)}`);
}
}, [user, loading, router]);
}, [user, loading, router, pathname]);
if (loading) {
return (

View file

@ -1,8 +1,9 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { useAuth } from "@/components/auth-provider";
import { isSafeNextPath } from "@/lib/safe-next-path";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -67,6 +68,7 @@ const labelStyle = { color: BRAND.textSecondary };
export default function AuthPage() {
const { login } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const [loading, setLoading] = useState(false);
const [loginEmail, setLoginEmail] = useState("");
@ -78,7 +80,8 @@ export default function AuthPage() {
try {
await login(loginEmail, loginPassword);
toast.success("Logged in successfully");
router.push("/dashboard");
const next = searchParams.get("next");
router.push(isSafeNextPath(next) ? next : "/dashboard");
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Login failed";
toast.error(message);

View file

@ -1,18 +1,19 @@
"use client";
import { useAuth } from "@/components/auth-provider";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, type ReactNode } from "react";
export function MobileAuthGate({ children }: { children: ReactNode }) {
const { user, loading } = useAuth();
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
if (!loading && !user) {
router.replace("/login");
router.replace(`/login?next=${encodeURIComponent(pathname)}`);
}
}, [user, loading, router]);
}, [user, loading, router, pathname]);
if (loading) {
return (

View file

@ -63,7 +63,7 @@ export default function BossDashboardView() {
const pb = getPocketBase();
if (!pb.authStore?.isValid) {
toast.error("Your session expired", { description: "Sign in again to continue." });
router.replace("/login");
router.replace(`/login?next=${encodeURIComponent("/m")}`);
return;
}
@ -85,7 +85,7 @@ export default function BossDashboardView() {
if (!res.ok) {
if (res.status === 401) {
toast.error("Your session expired", { description: "Sign in again to continue." });
router.replace("/login");
router.replace(`/login?next=${encodeURIComponent("/m")}`);
return;
}
throw new Error(`HTTP ${res.status}`);

View file

@ -0,0 +1,9 @@
/**
* Validates a `next` redirect target so login can send the user back
* to where they came from without opening a redirect to an external
* site (e.g. `?next=https://evil.example`). Only same-origin,
* root-relative paths are allowed.
*/
export function isSafeNextPath(path: string | null | undefined): path is string {
return !!path && path.startsWith("/") && !path.startsWith("//");
}