UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

286 lines (253 loc) 7.68 kB
import { NextAuthConfig } from "next-auth"; import { PrismaAdapter } from "@auth/prisma-adapter"; import { PrismaClient, UserRole } from "@prisma/client"; import Credentials from "next-auth/providers/credentials"; import GitHub from "next-auth/providers/github"; import Google from "next-auth/providers/google"; import bcrypt from "bcryptjs"; import { z } from "zod"; // Prisma 클라이언트 인스턴스 (실제로는 별도 파일에서 import) declare global { // eslint-disable-next-line no-var var __prisma: PrismaClient | undefined; } const prisma = globalThis.__prisma || new PrismaClient(); if (process.env.NODE_ENV !== "production") globalThis.__prisma = prisma; // 로그인 스키마 검증 const loginSchema = z.object({ email: z.string().email("유효한 이메일을 입력해주세요"), password: z.string().min(8, "비밀번호는 최소 8자 이상이어야 합니다"), }); // NextAuth.js 설정 export const authConfig: NextAuthConfig = { adapter: PrismaAdapter(prisma), providers: [ // 이메일/패스워드 인증 Credentials({ name: "credentials", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { try { // 입력 검증 const validatedFields = loginSchema.safeParse(credentials); if (!validatedFields.success) { console.error("Invalid credentials format"); return null; } const { email, password } = validatedFields.data; // 사용자 조회 const user = await prisma.user.findUnique({ where: { email }, select: { id: true, email: true, password: true, name: true, image: true, role: true, isActive: true, isLocked: true, failedLogins: true, lockedAt: true, }, }); if (!user) { console.error("User not found"); return null; } // 계정 상태 확인 if (!user.isActive) { console.error("Account is inactive"); return null; } if (user.isLocked) { console.error("Account is locked"); return null; } // 패스워드 검증 if (!user.password) { console.error("Password not set for user"); return null; } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { // 로그인 실패 횟수 증가 const newFailedLogins = user.failedLogins + 1; const shouldLock = newFailedLogins >= 5; await prisma.user.update({ where: { id: user.id }, data: { failedLogins: newFailedLogins, isLocked: shouldLock, lockedAt: shouldLock ? new Date() : null, }, }); // 감사 로그 기록 await prisma.auditLog.create({ data: { action: "LOGIN_FAILED", resource: "User", resourceId: user.id, userId: user.id, details: { failedAttempts: newFailedLogins, locked: shouldLock, }, }, }); console.error("Invalid password"); return null; } // 로그인 성공 시 실패 횟수 초기화 및 마지막 로그인 시간 업데이트 await prisma.user.update({ where: { id: user.id }, data: { failedLogins: 0, lastLoginAt: new Date(), }, }); // 감사 로그 기록 await prisma.auditLog.create({ data: { action: "LOGIN_SUCCESS", resource: "User", resourceId: user.id, userId: user.id, details: { loginMethod: "credentials", }, }, }); return { id: user.id, email: user.email, name: user.name, image: user.image, role: user.role, }; } catch (error) { console.error("Authentication error:", error); return null; } }, }), // GitHub OAuth GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, profile(profile) { return { id: profile.id.toString(), name: profile.name || profile.login, email: profile.email, image: profile.avatar_url, role: UserRole.VIEWER, // 기본 역할 }; }, }), // Google OAuth Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, profile(profile) { return { id: profile.sub, name: profile.name, email: profile.email, image: profile.picture, role: UserRole.VIEWER, // 기본 역할 }; }, }), ], callbacks: { async jwt({ token, user, trigger, session }) { // 초기 로그인 시 사용자 정보를 토큰에 추가 if (user) { token.role = user.role; token.id = user.id; } // 세션 업데이트 시 if (trigger === "update" && session) { token = { ...token, ...session }; } return token; }, async session({ session, token }) { // 토큰에서 세션으로 정보 전달 if (token) { session.user.id = token.id as string; session.user.role = token.role as UserRole; } return session; }, async signIn({ user, account, _profile }) { try { // OAuth 로그인 시 감사 로그 if (account?.provider !== "credentials") { await prisma.auditLog.create({ data: { action: "LOGIN_SUCCESS", resource: "User", resourceId: user.id, userId: user.id, details: { loginMethod: account?.provider, provider: account?.provider, }, }, }); } return true; } catch (error) { console.error("SignIn callback error:", error); return false; } }, }, pages: { signIn: "/auth/signin", signUp: "/auth/signup", error: "/auth/error", verifyRequest: "/auth/verify-request", newUser: "/auth/new-user", }, session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30일 updateAge: 24 * 60 * 60, // 24시간마다 업데이트 }, cookies: { sessionToken: { name: "agentc.session-token", options: { httpOnly: true, sameSite: "lax", path: "/", secure: process.env.NODE_ENV === "production", }, }, }, events: { async signOut({ token }) { // 로그아웃 감사 로그 if (token?.id) { await prisma.auditLog.create({ data: { action: "LOGOUT", resource: "User", resourceId: token.id as string, userId: token.id as string, details: {}, }, }); } }, }, debug: process.env.NODE_ENV === "development", trustHost: true, }; export default authConfig;