NextJS Lucia Auth 사용해보기

NextJS 프로젝트에 NextAuth를 사용하면서 5버전에서는 분명 편하지만 몇가지 마음에 안드는 부분들 때문에 Lucia Auth를 사용해보기로 했다.

Nextjs 강의와 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('/');
}

서버로부터 세션 정보 받기

  1. 프로바이더 작성
'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);
  1. 서버로부터 세션 전달받기
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")
}

Leave a Comment