프론트,백엔드 에서 모두 Prisma를 사용한다. 그 전의 프로젝트를 진행하면서는 프론트에서 Prisma를 사용해 DB(supabase)를 모두 접근하는 방식으로 하였다. 이번에는 백엔드까지 구축하려고 한다. 그래서 프론트엔드와 백엔드 에서의 Prisma를 이용해 어떤것을 하는지 알아보자
pnpm을 사용중이라 이렇게 설치한다.
pnpm dlx 는 npx랑 같은 것
frontend로 이동하여 해야함.
pnpm add next-auth@beta
pnpm dlx auth secret
env 파일에 AUTH_SECRET 생성됨.
import NextAuth from "next-auth"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [],
})
로그인 / 로그아웃 / 세션 확인을 위한 함수들을 만들어서 앱 전체에서 쓰게 해주는 파일
providers에 credentials,kakao 등 들어갈 수 있음.
import { handlers } from "@/auth"
export const { GET, POST } = handlers
NextAuth 인증 로직을 API 라우트에 연결하는 코드
export { auth as middleware } from "@/auth"
middleware 이곳에선 로그인 안한 사용자를 특정 페이지에서 막아주는 역할을 해줌
요청이 들어올 때 페이지 들어가기 전에 검사하는 부분이다.
pnpm add @prisma/client @auth/prisma-adapter
pnpm add prisma --save-dev
그 다음에 env 파일에 backend에서 설정했던 DATABASE_URL 설정 해줘야함.
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
PrismaClient를 한 번만 만들고
global에 저장해서 계속 재사용하도록 해주는 코드
https://authjs.dev/getting-started/adapters/prisma
참고해서 사용했습니다.
User : 사용자의 정보를 담는 Table
Account : 유저가 어떤 소셜 계정으로 로그인했는지 연결
Session : 세션 로그인 시 사용
VerificationToken : 이메일 인증 토큰
아래는 예시이므로 추가해도됨. 하지만 필드를 삭제하거나 관계를 꺠는것은 하지않는게 좋음.
model Account {
id String @id @default(cuid())
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime? @map("email_verified")
image String?
accounts Account[]
sessions Session[]
@@map("users")
}
model VerificationToken {
identifier String
token String
expires DateTime
@@unique([identifier, token])
@@map("verification_tokens")
}
이메일/비밀번호 기반 로그인을 구현하기 위해NextAuth의 Credentials Provider와 Prisma Adapter를 사용함. 비밀번호 검증과 사용자 조회 로직을 서버에서 처리하도록 구성했다.
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/prisma";
import CredentialsProvider from "next-auth/providers/credentials";
import { comparePassword } from "./lib/password-utils";
export const { handlers, auth, signIn, signOut } = NextAuth({
useSecureCookies: process.env.NODE_ENV === "production",
trustHost: true,
adapter: PrismaAdapter(prisma),
secret: process.env.AUTH_SECRET,
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email", placeholder: "Email" },
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials) {
if (!credentials) return null;
const email = credentials.email as string;
const password = credentials.password as string;
if (!email || !password) {
throw new Error("Invalid credentials");
}
const user = await prisma.user.findUnique({
where: { email },
});
if (!user || !user.hashedPassword) {
throw new Error("Invalid credentials");
}
const passwordMatch = await comparePassword(
password,
user.hashedPassword,
);
if (!passwordMatch) {
throw new Error("Invalid credentials");
}
return user;
},
}),
],
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
},
});
여기서 핵심은 authorize 함수이다. 살펴보면 받아온 email과 password로 user 테이블에서 같은 email을 찾고 비밀번호를 비교한다.
import bcrypt from "bcryptjs";
// salt + hash password
export const comparePassword = async (
password: string,
hash: string,
): Promise<boolean> => {
return bcrypt.compare(password, hash);
};
comparePassword 함수를 사용해 비교한다.
맞는게 없다면 에러 맞는게 있다면 user를 반환 즉 로그인 성공 또한 jwt를 사용한다. jwt를 사용하면 서버 세션 저장 없이 인증 가능 하며 확장성과 성능 측면에서 유리하다.
또한 adapter: PrismaAdapter(prisma),를 사용하면 NextAuth와 DB연결을 쉽게 도와준다.
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
signIn("credentials", {
email,
password,
redirectTo: "/",
});
};
signIn("credentials")를 통해 email과 password를 보내준 후 성공 시 /(main) 으로 가게 한다.
const existingUser = await prisma.user.findUnique({
where: {
email,
},
});
if (existingUser) {
return { status: "error", message: "이미 존재하는 이메일입니다." };
}
const user = await prisma.user.create({
data: {
email,
hashedPassword: await hashSaltPassword(password),
},
});
전달받은 email과 password로 진행된다.
email로 user table에 email과 같은 user를 찾고 있다면 return 해준다. 없다면 user.create를 사용해 유저를 생성 시켜줌. email과 hashSaltPassword 함수를 통해 만들어진 password를 통해서
export const hashSaltPassword = async (password: string): Promise<string> => {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
const hash = await bcrypt.hash(password, salt);
return hash;
};
import { signOut } from "@/auth";
await signOut();
signOut을 사용하면 바로 로그아웃이 가능함.