@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
444 lines (393 loc) • 9.58 kB
text/typescript
import bcrypt from "bcryptjs";
import crypto from "crypto";
import { PrismaClient } from "@prisma/client";
// Prisma 클라이언트 인스턴스
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;
// 타입 정의
export interface CreateUserData {
email: string;
password: string;
name?: string;
role?: string;
tenantId?: string;
}
export interface PasswordResetData {
email: string;
token: string;
newPassword: string;
}
export interface EmailVerificationData {
email: string;
token: string;
}
/**
* 비밀번호 해싱
*/
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
/**
* 비밀번호 검증
*/
export async function verifyPassword(
password: string,
hashedPassword: string
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
/**
* 보안 토큰 생성
*/
export function generateSecureToken(length: number = 32): string {
return crypto.randomBytes(length).toString("hex");
}
/**
* 새 사용자 등록
*/
export async function createUser(userData: CreateUserData) {
try {
// 이메일 중복 확인
const existingUser = await prisma.user.findUnique({
where: { email: userData.email },
});
if (existingUser) {
throw new Error("이미 존재하는 이메일입니다");
}
// 비밀번호 해싱
const hashedPassword = await hashPassword(userData.password);
// 이메일 검증 토큰 생성
const emailVerificationToken = generateSecureToken();
// 사용자 생성
const user = await prisma.user.create({
data: {
email: userData.email,
password: hashedPassword,
name: userData.name,
role: (userData.role as any) || "VIEWER",
tenantId: userData.tenantId,
isActive: false, // 이메일 검증 전까지 비활성화
},
});
// 이메일 검증 토큰 저장
await prisma.verificationToken.create({
data: {
identifier: userData.email,
token: emailVerificationToken,
expires: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24시간 후 만료
},
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: "USER_CREATED",
resource: "User",
resourceId: user.id,
userId: user.id,
details: {
email: userData.email,
role: userData.role || "VIEWER",
},
},
});
return {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
emailVerificationToken,
};
} catch (error) {
console.error("Create user error:", error);
throw error;
}
}
/**
* 이메일 검증
*/
export async function verifyEmail(data: EmailVerificationData) {
try {
// 검증 토큰 확인
const verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: data.email,
token: data.token,
expires: {
gt: new Date(),
},
},
});
if (!verificationToken) {
throw new Error("유효하지 않거나 만료된 검증 토큰입니다");
}
// 사용자 활성화
const user = await prisma.user.update({
where: { email: data.email },
data: {
isActive: true,
emailVerified: new Date(),
},
});
// 사용된 토큰 삭제
await prisma.verificationToken.delete({
where: {
identifier_token: {
identifier: data.email,
token: data.token,
},
},
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: "EMAIL_VERIFIED",
resource: "User",
resourceId: user.id,
userId: user.id,
details: {
email: data.email,
},
},
});
return user;
} catch (error) {
console.error("Email verification error:", error);
throw error;
}
}
/**
* 패스워드 리셋 토큰 생성
*/
export async function generatePasswordResetToken(email: string) {
try {
// 사용자 확인
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
throw new Error("존재하지 않는 사용자입니다");
}
// 기존 토큰 삭제
await prisma.passwordResetToken.deleteMany({
where: { email },
});
// 새 토큰 생성
const token = generateSecureToken();
const expires = new Date(Date.now() + 60 * 60 * 1000); // 1시간 후 만료
await prisma.passwordResetToken.create({
data: {
email,
token,
expires,
},
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: "PASSWORD_RESET_REQUESTED",
resource: "User",
resourceId: user.id,
userId: user.id,
details: {
email,
},
},
});
return token;
} catch (error) {
console.error("Generate password reset token error:", error);
throw error;
}
}
/**
* 패스워드 리셋
*/
export async function resetPassword(data: PasswordResetData) {
try {
// 리셋 토큰 확인
const resetToken = await prisma.passwordResetToken.findFirst({
where: {
email: data.email,
token: data.token,
expires: {
gt: new Date(),
},
},
});
if (!resetToken) {
throw new Error("유효하지 않거나 만료된 리셋 토큰입니다");
}
// 새 비밀번호 해싱
const hashedPassword = await hashPassword(data.newPassword);
// 비밀번호 업데이트
const user = await prisma.user.update({
where: { email: data.email },
data: {
password: hashedPassword,
failedLogins: 0, // 실패 횟수 초기화
isLocked: false, // 잠금 해제
lockedAt: null,
},
});
// 사용된 토큰 삭제
await prisma.passwordResetToken.delete({
where: {
id: resetToken.id,
},
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: "PASSWORD_RESET_COMPLETED",
resource: "User",
resourceId: user.id,
userId: user.id,
details: {
email: data.email,
},
},
});
return user;
} catch (error) {
console.error("Reset password error:", error);
throw error;
}
}
/**
* 사용자 계정 잠금 해제
*/
export async function unlockUser(userId: string) {
try {
const user = await prisma.user.update({
where: { id: userId },
data: {
isLocked: false,
lockedAt: null,
failedLogins: 0,
},
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: "USER_UNLOCKED",
resource: "User",
resourceId: userId,
userId: userId,
details: {},
},
});
return user;
} catch (error) {
console.error("Unlock user error:", error);
throw error;
}
}
/**
* 사용자 역할 변경
*/
export async function changeUserRole(
userId: string,
newRole: string,
changedBy: string
) {
try {
const oldUser = await prisma.user.findUnique({
where: { id: userId },
select: { role: true },
});
if (!oldUser) {
throw new Error("사용자를 찾을 수 없습니다");
}
const user = await prisma.user.update({
where: { id: userId },
data: { role: newRole as any },
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: "USER_ROLE_CHANGED",
resource: "User",
resourceId: userId,
userId: changedBy,
details: {
oldRole: oldUser.role,
newRole: newRole,
targetUserId: userId,
},
},
});
return user;
} catch (error) {
console.error("Change user role error:", error);
throw error;
}
}
/**
* 사용자 프로필 업데이트
*/
export async function updateUserProfile(
userId: string,
updateData: { name?: string; image?: string }
) {
try {
const user = await prisma.user.update({
where: { id: userId },
data: updateData,
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: "USER_PROFILE_UPDATED",
resource: "User",
resourceId: userId,
userId: userId,
details: {
updatedFields: Object.keys(updateData),
},
},
});
return user;
} catch (error) {
console.error("Update user profile error:", error);
throw error;
}
}
/**
* 사용자 활성/비활성 토글
*/
export async function toggleUserStatus(
userId: string,
isActive: boolean,
changedBy: string
) {
try {
const user = await prisma.user.update({
where: { id: userId },
data: { isActive },
});
// 감사 로그 기록
await prisma.auditLog.create({
data: {
action: isActive ? "USER_ACTIVATED" : "USER_DEACTIVATED",
resource: "User",
resourceId: userId,
userId: changedBy,
details: {
targetUserId: userId,
isActive,
},
},
});
return user;
} catch (error) {
console.error("Toggle user status error:", error);
throw error;
}
}