Search code examples
next.jsnext.js13next-auth

How can I implement "redirect to history/previous dynamic route after successful login" using Nextauth


I am trying to implement a system where user will redirected to previous protected dynamic route after successful login. Let me explain briefly. First I provide you the folder structure, Login component and Nextauth settings

Using

"next-auth": "^4.22.3",
"next": "13.4.5",

Here is the folder structure for better understanding

πŸ“¦src
 ┣ πŸ“‚app
 ┃ ┣ πŸ“‚(auth)
 ┃ ┃ ┣ πŸ“‚login
 ┃ ┃ ┃ β”— πŸ“œpage.tsx
 ┃ ┃ ┣ πŸ“‚register
 ┃ ┃ ┃ β”— πŸ“œpage.tsx 
 ┃ ┣ πŸ“‚(private_route)
 ┃ ┃ ┣ πŸ“‚animated-card
 ┃ ┃ ┃ β”— πŸ“‚[id]
 ┃ ┃ ┃ ┃ ┣ πŸ“œpage.jsx
 ┃ ┃ ┃ ┃ ┣ πŸ“œreadme.md
 ┃ ┃ ┃ ┃ β”— πŸ“œreadme2.md
 ┃ ┃ ┣ πŸ“‚image-editor
 ┃ ┃ ┃ β”— πŸ“‚[id]
 ┃ ┃ ┃ ┃ ┣ πŸ“œpage.jsx
 ┃ ┃ ┣ πŸ“‚let-us-design-for-you
 ┃ ┃ ┃ β”— πŸ“‚[id]
 ┃ ┃ ┃ ┃ β”— πŸ“œpage.jsx
 ┃ ┣ πŸ“‚api
 ┃ ┃ ┣ πŸ“‚auth
 ┃ ┃ ┃ ┣ πŸ“‚users
 ┃ ┃ ┃ ┃ β”— πŸ“œroute.js
 ┃ ┃ ┃ β”— πŸ“‚[...nextauth]
 ┃ ┃ ┃ ┃ β”— πŸ“œroute.js

api/auth/[...nextauth]/route.js

import startDb from "@/lib/db";
import UserModel from "../../../../models/userModel";
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";

export const authOptions = {
  session: {
    strategy: "jwt",
  },
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    FacebookProvider({
      clientId: process.env.FACEBOOK_APP_ID,
      clientSecret: process.env.FACEBOOK_APP_SECRET,
    }),
    CredentialsProvider({
      name: "Credentials",
      credentials: {},
      async authorize(credentials, req) {
        const { email, password } = credentials;

        await startDb();

        const user = await UserModel.findOne({ email });

        if (!user) throw new Error("email/password mismatch!");

        const passwordMatch = await user.comparePassword(password);
        if (!passwordMatch) throw new Error("email/password mismatch");

        return {
          id: user._id.toString(),
          email: user.email,
          name: user.name,
          role: user.role,
        };
      },
    }),
  ],
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/login",
    signOut: "/login",
    error: "/login",
  },
  callbacks: {
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id;
        session.user.role = token.role;
      }

      return session;
    },

    jwt(params) {
      if (params.user?.role) {
        params.token.role = params.user.role;
        params.token.id = params.user.id;
      }
      return params.token;
    },
  },
};

const authHandler = NextAuth(authOptions);

module.exports = {
  GET: authHandler,
  POST: authHandler,
};

and Finally the Login Component

"use client";

import Image from "next/image";
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";

const Login = () => {
  const router = useRouter();

  const [showToast, setShowToast] = useState(false);
  const [type, setType] = useState("");
  const [busy, setBusy] = useState(false);
  const [userInfo, setUserInfo] = useState({
    email: "",
    password: "",
  });
  const [error, setError] = useState("");
  const { password, email } = userInfo;

  const handleChange = ({ target }) => {
    const { name, value } = target;
    setUserInfo({ ...userInfo, [name]: value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    // console.log(userInfo);

    const res = await signIn("credentials", {
      email,
      password,
      redirect: false,
    });

    if (res?.error) {
      setType("error");
      setError("Login Failed.");
      setShowToast(true);
      return;
    }
    // If login is successful
    setType("success");
    setError("Login Success.");
    setShowToast(true);
    router.push("/");
  };

  const handleToastClose = () => {
    setShowToast(false);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <div className="form-control  join-item text-base">
          <div className="mb-2">
            <label className="label">
              <span className="text-base">Username or Email *</span>
            </label>
            <input
              type="text"
              onChange={handleChange}
              value={email}
              name="email"
              placeholder="[email protected]"
              className="input input-bordered w-full h-11 placeholder:italic placeholder:font-normal"
            />
          </div>
          <div>
            <label className="label">
              <span className="text-base">Password *</span>
            </label>
            <input
              type="password"
              value={password}
              name="password"
              onChange={handleChange}
              placeholder="Password at least 6 Characters long"
              className="input input-bordered w-full h-11 placeholder:italic placeholder:font-normal"
            />
          </div>
          <button className="btn btn-primary mt-4 mb-6 text-white text-base capitalize font-medium">
            Sign In
          </button>
        </div>
      </form>
    </div>
  );
};

export default Login;

When a non-user attempts to access these protected routes, they are sent to the login page. These are the protected routes

  1. (private_route)/animated-card/[id]/page.jsx
  2. (private_route)/image-editor/[id]/page.jsx
  3. (private_route)/let-us-design-for-you/[id]/page.jsx

After successful login they will be redirected to home page. But I dont want that. I want them to redirect them to the protected page where they were last redirected.

I tried to follow the next auth documentation. But did not understand.


Solution

  • I guess the next auth is having some bugs with their app router version. One approach is to set a callbackUrl for each pages and you have to do that on the page level instead of doing it on the layout level. I'm sharing a sample code (assuming that all your pages are server-component):

    (private_route)/animated-card/[id]/page.jsx
    import { getServerSession } from 'next-auth/next'
    import { redirect } from 'next/navigation'
    import { authOptions } from "../api/auth/[...nextauth]/route";
    
    const ProtectedPage = async ({params}) => {
      const session = await getServerSession(authOptions)
      if (!session) {
        redirect(`/login?callbackUrl=/animated-card/${params.id}`)
      }
      return (
        <section className='py-24'>
          <div className='container'>
              Hello from animated card{params.id}..
          </div>
        </section>
      )
    }
    
    export default ProtectedPage
    

    Here, we're passing the url of the specific page that user is trying to access as a callback url and after successful login, user will be redirected to that url.

    checkout this git repo for more: https://github.com/foadtkf/next-auth-playground