FD3
FD3

Reputation: 1956

Preventing direct access to a page in Next.js while allowing access from within the app

I'm working on a Next.js application, and I have a specific page, let's call it "thank-you," that I want to prevent users from accessing directly via the URL. However, I want to allow access to this page only through internal navigation within the app, such as clicking a button.

I've tried using a middleware function to redirect direct requests to the home page, and it works as expected. However, when I try to navigate to the "thank-you" page using client-side routing (e.g., router.push("/thank-you")), it still triggers the middleware and redirects to the home page.

Here's a simplified version of the code:

Middleware Function (middleware.ts):

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL("/", request.url));
}

export const config = {
  matcher: ["/thank-you"],
};

Button Click Handling (YourComponent.tsx):

import { useRouter } from "next/router";

export default function Home() {
  const router = useRouter();

  const handleButtonClick = () => {
    router.push("/thank-you");
  };
...
}

How can I prevent direct access to the "thank-you" page while allowing access through internal navigation within the app?

Any help or suggestions would be greatly appreciated!

UPDATE

I tried with getServerSideProps and still have the same problem. Redirect to home happens from button click as well.

export const getServerSideProps = () => {
  return {
    redirect: {
      permanent: false,
      destination: "/",
    },
    props: {},
  };
};

Upvotes: 2

Views: 4562

Answers (3)

3limin4t0r
3limin4t0r

Reputation: 21140

You can use the Referer header like the answer of Ilya Khrustalev suggests. To do this using middleware, you can simply check if the referer origin matches the request origin. Aka the request is coming from your own site.

Note that the Referer header is normally set by the web-browser, but can be manually set by a user. So this is not the way to go regarding security. However if you simply want to redirect a user back when pasting an URL in the address bar this would do fine. Just realize that whatever is located at the URL is still public.

With the warning out of the way let's get to the code.

// middleware.js
import { NextResponse } from 'next/server'
 
export const config = { matcher: '/thank-you' };

export function middleware(request) {
  try {
    const requestURL = new URL(request.url);
    const refererURL = new URL(request.headers.get("referer"));

    if (refererURL.origin !== requestURL.origin) {
      throw new Error("origin mismatch");
    }
  } catch {
    // executed if one of the URLs fails to parse, or if origin doesn't match
    NextResponse.redirect(new URL('/', request.url));
  }
}

Since new URL(request.headers.get("referer")) can failed to parse if the Referer header is not given or invalid I've used a try-catch statement for the redirect.

For additional info about multiple/dynamic route matches see the Next.js Middleware documentation.

Upvotes: 2

3limin4t0r
3limin4t0r

Reputation: 21140

Maybe this is a bit too simple, but why expose a route to the component when don't want it accessible via a direct URL?

The simplest solution is to avoid the issue entirely and not expose an URL for the "thank you" page. You can do this by creating a <ThankYou /> component, and render that on the current page as part of some procedure.

This can be done by toggling some state (showThankYou for example):

export default function Page() {
  const [showThankYou, setShowThankYour] = useState(false);

  // ...

  if (showThankYou) return <ThankYou />;

  return (
    <NormalPage />
  );
}

Or by returning it as part of a more complicated process:

export enum SignUp {
  None,
  UserInfo,
  ThankYou,
}

export default function Page() {
  const [signUp, setSignUp] = useState(SignUp.None);
  const [_context, setContext] = useState({});
  const context = useMemo(() => ({ ..._context, setContext }), [_context]);

  const navigateSignUp = useCallback(() => setSignUp(SignUp.UserInfo), []);
  const navigateThankYou = useCallback(() => setSignUp(SignUp.ThankYou), []);

  switch (signUp) {
    case SignUp.UserInfo:
      return <CollectUserInfo {...context} navigateThankYou={navigateThankYou} />;
    case SignUp.ThankYou:
      return <ThankYou {...context} />;
    default:
      return <InformationPage {...context} navigateSignUp={navigateSignUp} />;
  }
}

Because this doesn't expose a route to the "thank you" page, users can only get there by following the procedure.

Upvotes: 0

Arnas Dičkus
Arnas Dičkus

Reputation: 677

This seems like auth-guard issue. Add AuthGuard in root folder. If using app Router it would be layout, and control access with redux or other state management.

I didn't include but you also should change authRights.thankYou to false in auth guard or in thank you page.

Root Layout

export default function RootLayout({
  children,
}: IPageParamsLayout) {
  return (
    <html>
      <body>
       <main>
        <AuthGuard>{children}</AuthGuard>
       </main>
      </body>
    </html>
  );
}

AuthGuard

"use client"
import { useRouter } from "next/navigation";
import { usePathname } from "next/navigation";

// Authguard would have all redirection logic to keep redirection in one place and prevent redirection loop.
const AuthGuard = (children) => {
const pathname = usePathname();
const router = useRouter();
const dispatch = useAppDispatch();
const authRights = useAppSelector(authRights);

  if(pathname === 'thank-you' && !authRights.thankYou) {
   router.push(`/`);
  }

  return (
    <>{children}</>
  )
}
export default AuthGuard

Component that redirects to Thank you.


"use client"

const BeforeThankYou = () => {
  const dispatch = useAppDispatch();
  return (
    <div onClick={() => {
      dispatch(setAuthRights({
       thankYou: true
       }))
     router.push("/thank-you")
    }}>Click to Thank You Page</div>

  )
}
export default BeforeThankYou

Upvotes: 0

Related Questions