NextJS 프로젝트에 NextAuth를 사용하면서 5버전에서는 분명 편하지만 몇가지 마음에 안드는 부분들 때문에 Lucia Auth를 사용해보기로 했다.
Nextjs 강의와 Lucia Auth 공식 문서를 보면서 구현 한 것을 작성함.
auth.ts 작성
import { prisma } from './prisma';
import { Lucia, TimeSpan } from 'lucia';
import { cookies } from 'next/headers';
import { PrismaAdapter } from '@lucia-auth/adapter-prisma';
const adapter = new PrismaAdapter(prisma.session, prisma.user);
const lucia = new Lucia(adapter, {
getUserAttributes: (attributes) => {
return {
email: attributes.email,
// name: user.name,
// image: user.image,
};
},
sessionCookie: {
expires: false,
attributes: {
secure: process.env.NODE_ENV === 'production',
},
},
sessionExpiresIn: new TimeSpan(2, 'w'), // 2 weeks
});
/* 로그인 사용 - 세션 생성 */
export async function createAuthSession(userId: string) {
const session = await lucia.createSession(userId, {});
const { name, value, attributes } = lucia.createSessionCookie(session.id);
const cookieStore = await cookies();
cookieStore.set(name, value, attributes);
}
/* 토큰 체크, 세션 유저 정보 반환 */
export async function verifyAuth() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get(lucia.sessionCookieName);
if (!sessionCookie) {
return {
session: null,
user: null,
};
}
const sessionId = sessionCookie.value;
if (!sessionId) {
return {
session: null,
user: null,
};
}
const result = await lucia.validateSession(sessionId);
try {
if (result.session && result.session.fresh) {
const { name, value, attributes } = lucia.createSessionCookie(result.session.id);
cookieStore.set(name, value, attributes);
}
if (!result.session) {
const { name, value, attributes } = lucia.createBlankSessionCookie();
cookieStore.set(name, value, attributes);
}
} catch (error) {}
return result;
}
/* 세션 제거 */
export async function destroySession() {
const cookieStore = await cookies();
const { session } = await verifyAuth();
if (!session) {
return {
error: 'unauthorized',
};
}
await lucia.invalidateSession(session.id);
const { name, value, attributes } = lucia.createBlankSessionCookie();
cookieStore.set(name, value, attributes);
}
lucia.d.ts 작성
import type { lucia } from 'lucia';
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
}
// interface Session extends UserAttributes {
// id: string;
// }
}
interface DatabaseSessionAttributes {
email: string;
}
로그인
'use server';
import { compare } from 'bcryptjs';
import { z } from 'zod';
import { redirect } from 'next/navigation';
import { createAuthSession } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
const signInSchema = z.object({
email: z
.string({
required_error: '이메일을 입력해 주세요.',
})
.min(1, {
message: '이메일을 입력해 주세요.',
})
.email({
message: '유효하지 않은 이메일입니다.',
}),
password: z.string({
required_error: '비밀번호을 입력해 주세요.',
}),
});
type FormState = {
errors: {
email?: string[];
password?: string[];
_formErrors?: string[];
};
};
export async function authenticateUser(formState: FormState, formData: FormData) {
const result = signInSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
});
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
};
}
const { email, password } = result.data;
const user = await prisma.user.findUnique({
where: {
email,
},
});
if (!user) {
return {
errors: {
email: ['유저를 찾을 수 없습니다.'],
},
};
}
const isValid = await compare(password, user.password);
if (!isValid) {
return {
errors: {
password: ['비밀번호를 틀렸습니다.'],
},
};
}
await createAuthSession(user.id);
redirect('/');
}
로그아웃
import { redirect } from 'next/navigation';
import { destroySession } from '@/lib/auth';
export default async function signOut() {
await destroySession();
redirect('/');
}
서버로부터 세션 정보 받기
- 프로바이더 작성
'use client';
import type { User, Session } from 'lucia';
import { createContext, useContext } from 'react';
const SessionContext = createContext({});
interface SessionProviderProps {
session?: {
user: User | null;
session: Session | null;
};
children: React.ReactNode;
}
export default function SessionProvider({ children, session = { user: null, session: null } }: SessionProviderProps) {
return <SessionContext.Provider value={session}>{children}</SessionContext.Provider>;
}
export const useSession = () => useContext(SessionContext);
- 서버로부터 세션 전달받기
import './globals.css';
import type { Metadata } from 'next';
import SessionProvider from '@/components/providers/session-provider';
import { verifyAuth } from '@/lib/auth';
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const session = await verifyAuth();
return (
<html lang="ko">
<body>
<SessionProvider session={session}>
<>{children}</>
</SessionProvider>
</body>
</html>
);
}
프리즈마
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
password String?
nickname String?
sessions Session[]
@@map("users")
}
model Session {
id String @id
userId String
expiresAt DateTime
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
@@map("sessions")
}