Search code examples
authenticationnext.jsnext-authnextjs14

Signout and login not working in auth.js in next14 with middleware and server actions


I am new to auth.js (previously next-auth) and I am trying to implement it in my next14 project with src and app directories. The folder structure of my app is like below

Folder structure

I have used prisma adapter with authjs and prisma is connected to my mongodb database

Now, currently I am facing 2 problems:

  1. The signout function is not working
  2. While logging in, if i provide wrong credentials my login page works fine, but if i provide correct credentials somehow the app just gets stuck in loading state and when i refresh the page i see that i am logged in.

If anyone could please help me out with any one of them I would be very grateful. Thank you.

Firstly for logout not working issue

Navbar.tsx (directly used in layout)

import { auth, signOut } from "@/auth";
import Image from "next/image";
import Link from "next/link";
import { BsPersonCircle } from "react-icons/bs";

export default async function Navbar() {
  const session = await auth();

  return (
    <div className="navbar bg-base-200 h-[10vh] px-[60px] py-[20px]">
      <Link href={"/"} className="flex-1">
        <Image src="/logo.png" alt="logo" width={200} height={200} />
      </Link>
      <div className="dropdown dropdown-end">
        <div className="flex items-center gap-3">
          <p>Hello {session?.user?.name ? session?.user?.name : "stranger"}</p>
          <div tabIndex={0} role="button" className="btn btn-ghost btn-circle">
            <BsPersonCircle size={30} color="#E84644" />
          </div>
        </div>
        <ul
          tabIndex={0}
          className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 border border-primary"
        >
          <li>
            <Link href={"/profile"} className="justify-between">
              Profile
              <span className="badge">New</span>
            </Link>
          </li>
          <li>
            <a>Settings</a>
          </li>
          <li>
            <form action={async () => {
              "use server";
              await signOut();
            }}>
              <button type="submit">
                Logout
              </button>
            </form>
          </li>
        </ul>
      </div>
    </div>
  );
}

auth.ts

import NextAuth from "next-auth";
import authConfig from "@/auth.config";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { db } from "./lib/db";

export const { handlers, auth, signIn, signOut } = NextAuth({
  debug: true,
  adapter: PrismaAdapter(db),
  session: { strategy: "jwt" },
  ...authConfig,
});

auth.config.ts

import GitHub from "next-auth/providers/github";
import type { NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { LoginSchema } from "./schemas";
import { getUserByEmail } from "./data/user";
import brcypt from "bcryptjs";

export default {
    providers: [
        GitHub,
        Credentials({
            authorize: async (credentials) => {
                const validatedFields = LoginSchema.safeParse(credentials);

                if(validatedFields.success) {
                    const { email, password } = validatedFields.data;

                    const user = await getUserByEmail(email);
                    if(!user || !user.password) return null;

                    const passwordsMatch = await brcypt.compare(password, user.password);
                    if(passwordsMatch) {
                        return user;
                    }
                }

                return null;
            }
        })
    ],
} satisfies NextAuthConfig;

Secondly for login getting stuck issue

app > auth > login > page.tsx

"use client";
import LoginForm from "@/Components/Common/Auth/LoginForm";
import AuthWrapper from "@/Components/Common/Auth/AuthWrapper";
import { useTransition } from "react";

export default function Page() {
  const [isPending, startTransition] = useTransition();

  return (
    <div>
      <AuthWrapper register={false} isPending={isPending}>
        <LoginForm isPending={isPending} startTransition={startTransition} />
      </AuthWrapper>
    </div>
  )
}

LoginForm.tsx

import React, { useState } from 'react';
import { useForm } from "react-hook-form";
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from "zod";
import { LoginSchema } from '@/schemas';
import FormToast from './FormToast';
import { login } from '@/actions/login';

export default function LoginForm({ isPending, startTransition }: { isPending: boolean, startTransition: any }) {
    const { register, handleSubmit, formState: { errors } } = useForm<z.infer<typeof LoginSchema>>({
        resolver: zodResolver(LoginSchema),
        defaultValues: {
            email: "",
            password: "",
        },
    });

    const [feedback, setFeedback] = useState<{
        error?: boolean
        msg?: string
    }>();

    const onSubmit = async (data: z.infer<typeof LoginSchema>) => {
        startTransition(async () => {
            setFeedback({
                error: undefined,
                msg: undefined,
            });
    
            const result: { error: boolean, msg: string } = await login(data);

            if(result) {
                setFeedback(result);
            }
        });
    };

    return (
        <div className="grid grid-cols-1 gap-5 w-[30vw]">
            
            <form onSubmit={handleSubmit(onSubmit)} className="grid grid-cols-1 gap-5">
                <div className="col-span-1">
                    <label className="input input-bordered input-md flex items-center gap-2">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4 opacity-70">
                            <path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
                            <path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
                        </svg>
                        <input
                            type="text"
                            className="grow"
                            placeholder="Email"
                            {...register("email")}
                            disabled={isPending}
                        />
                    </label>
                    {errors.email && <ErrorMsg msg={errors.email.message} />}
                </div>

                <div className="col-span-1">
                    <label className="input input-bordered input-md flex items-center gap-2">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4 opacity-70">
                            <path fillRule="evenodd" d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z" clipRule="evenodd" />
                        </svg>
                        <input
                            type="password"
                            className="grow"
                            placeholder="Password"
                            {...register("password")}
                            disabled={isPending}
                        />
                    </label>
                    {errors.password && <ErrorMsg msg={errors.password.message} />}
                </div>
                <FormToast {...feedback} />
                <button disabled={isPending} type="submit" className="col-span-1 btn btn-primary text-white">Login</button>
            </form>
        </div>
    );
}

function ErrorMsg( { msg }: { msg: string | undefined }) {
    return (
        <p className="text-primary text-sm text-left mt-1">{msg}</p>
    )
}

I am using server actions for login

src > actions > login.ts

"use server";
import { signIn } from "@/auth";
import { DEFAULT_LOGIN_REDIRECT } from "@/routes";
import { LoginSchema } from "@/schemas";
import { AuthError } from "next-auth";
import * as z from "zod";

export const login = async (values: z.infer<typeof LoginSchema>) => {
    const validatedFields = LoginSchema.safeParse(values);

    if (!validatedFields.success) {
        return { error: true, msg: "Invalid fields" };
    }

    const { email, password } = validatedFields.data;

    try {
        await signIn("credentials", {
            email,
            password,
        });

        return { error: false, msg: "Logged in successfully!" }
    } catch (error) {
        if (error instanceof AuthError) {
            switch (error.type) {
                case "CredentialsSignin": return {
                    error: true,
                    msg: "Invalid credentials"
                }
                default: return { error: true, msg: "Something went wrong!" }
            }
        }

        throw error;
    }

}

I am also providing the middleware just in case i did something wrong here middleware.ts

import authConfig from "@/auth.config"
import NextAuth from "next-auth"
import { DEFAULT_LOGIN_REDIRECT, apiAuthPrefix, authRoutes, publicRoutes } from "./routes"

const { auth } = NextAuth(authConfig)

export default auth((req) => {    
    const { nextUrl } = req;
    const isLoggedIn = !!req.auth;
    const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix);
    const isPublicRoute = publicRoutes.includes(nextUrl.pathname);
    const isAuthRoute = authRoutes.includes(nextUrl.pathname);
    

    if (isApiAuthRoute) {
        return;
    }

    if (isAuthRoute) {
        if (isLoggedIn) {
            return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
        }
        return;
    }

    if (!isLoggedIn && !isPublicRoute) {
        return Response.redirect(new URL("/auth/login", nextUrl));
    }

    return;
})

export const config = {
    matcher: ["/((?!api|_next/static|_next/image|favicon.ico|logo.png|logoVertical.png|logoVertical2.png).*)"],
}

Solution

  • So if someone is using middlewares like me to handle redirections after login and logout the only solution to the problems here is:

    1. Use client signOut function instead of server action provided by auth.js.
    2. Don’t use useTransition with login server action, just use simple loading state if needed.

    I still don't understand why does the redirection was not happening previously in both cases even though the logs were showing GET request.