Web Framework & Library/Next.js

[Next.js] NextAuth + sendmail 을 이용한 메일 인증 기반 회원가입 기능 구현 --- (2) NextAuth 설정

노호호 2025. 2. 4. 17:09

 

NextAuth.js는 Next.js 애플리케이션들을 위한 오픈소스 인증 솔루션이다.

Google, Kakao 등 많은 서드파티 서비스들을 쉽게 연계할 수 있게 해주고, Prisma 등 각종 ORM에 대한 지원도 잘해주는 툴이다.

나는 서드파티 fetch 없이 순수 이메일 가입 + Prisma + MySQL 형식으로 구현했다.


1. NextAuth 설치

아래 명령어를 사용해 NextAuth 및 Prisma 관련 패키지를 설치한다.

npm install next-auth @prisma/client @auth/prisma-adapter
npm install prisma --save-dev

 

 


2. app/api/auth/[...nextauth]/route.js 작성

그 다음엔 api/api/auth/[...nextauth]/ 디렉토리를 만든 후 route.js를 작성한다.

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaClient } from "@prisma/client";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import bcrypt from "bcrypt";

const prisma = new PrismaClient();

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        username: { label: "Username", type: "text" },
        password: { label: "Password", type: "password" }
      },
      authorize: async (credentials) => {
        try {
          console.log("Credentials received:", credentials);
          
          const user = await prisma.user.findUnique({
            where: { username: credentials.username }
          });

          console.log("User found:", user);

          if (user) {
            const isValidPassword = await bcrypt.compare(credentials.password, user.password);

            console.log("Password valid:", isValidPassword);
            console.log("user:", user);

            if (isValidPassword) {
              return { id: user.id, email: user.email, name: user.username };
            }
          }
          return null;
        } catch (error) {
          console.error("Error in authorize function:", error);
          return null;
        }
      }
    })
  ],
  adapter: PrismaAdapter(prisma),
  secret: process.env.NEXTAUTH_SECRET,
  session: {
    jwt: true,
  },
  callbacks: {
    async session({ session, token }) {
      session.user.id = token.id;
      session.user.username = token.username;
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.username = user.username;
        console.log("JWT token after login:", token);
      }
      return token;
    }
  },
  pages: {
    signIn: '/login',
  }
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

 


3. /login 페이지 연동

/app/login/page.js를 작성해서, 위에서 설정한 대로 로그인 페이지에서 signIn 이 가능하게 하면 된다.

"use client";

import { useState } from 'react';
import Link from 'next/link';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import Header from '../../../components/Header';
import Footer from '../../../components/Footer';

const LoginComponent = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await signIn("credentials", {
      redirect: false,
      username,
      password
    });

    if (result.ok) {
      if (username === 'admin') {
        router.push('/admin-dashboard');
      } else {
        router.push('/');
      }
    } else {
      setError("Login failed. Please check your username and password.");
    }
  };

  return (
    <div className="flex flex-col min-h-screen">
      <Header />
      <main className="flex-1 flex flex-col items-center justify-center bg-card">
        <h1 className="text-4xl bricolage-grotesque mb-6">Login Page</h1>
        <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-md w-full max-w-md">
          <div className="mb-4">
            <label htmlFor="username" className="block text-sm font-medium text-gray-700">
              Username
            </label>
            <input
              type="text"
              id="username"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 text-black rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
              required
            />
          </div>
          <div className="mb-6">
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Password
            </label>
            <input
              type="password"
              id="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 text-black rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
              required
            />
          </div>
          {error && <p className="text-red-500 text-sm mb-4">{error}</p>}
          <button
            type="submit"
            className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-black hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
          >
            Login
          </button>
          <div className="mt-4 flex justify-center space-x-2 text-primary">
            <Link href="/signup">
              <p className="text-lg font-medium hover:underline">Sign Up</p>
            </Link>
            <span>|</span>
            <Link href="/request-recovery?type=username">
              <p className="text-sm hover:underline">Find Username</p>
            </Link>
            <span>|</span>
            <Link href="/request-recovery?type=password">
              <p className="text-sm hover:underline">Find Password</p>
            </Link>
          </div>
        </form>
      </main>
      <Footer />
    </div>
  );
};

export default LoginComponent;