@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
286 lines (253 loc) • 7.68 kB
text/typescript
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;