'use server'; import { z } from 'zod'; import { eq ,and} from 'drizzle-orm'; import db from '@/db/db'; import { users, mfaTokens } from '@/db/schema'; import crypto from 'crypto'; import { PasswordStrength, passwordResetRequestSchema, passwordResetSchema } from './validataions-password'; import { sendEmail } from '@/lib/mail/sendEmail'; import { analyzePasswordStrength, checkPasswordHistory, validatePasswordPolicy } from '@/lib/users/auth/passwordUtil'; import { headers } from 'next/headers'; export interface PasswordValidationResult { strength: PasswordStrength; policyValid: boolean; policyErrors: string[]; historyValid?: boolean; } // 비밀번호 재설정 요청 (서버 액션) export async function requestPasswordResetAction( prevState: any, formData: FormData ): Promise<{ success: boolean; error?: string; message?: string }> { try { const rawData = { email: formData.get('email') as string, }; const validatedData = passwordResetRequestSchema.parse(rawData); // 사용자 존재 확인 const user = await db .select({ id: users.id, name: users.name, email: users.email, language: users.language }) .from(users) .where(eq(users.email, validatedData.email)) .limit(1); if (!user[0]) { // 보안상 사용자가 없어도 성공한 것처럼 응답 return { success: true, message: '해당 이메일로 재설정 링크를 전송했습니다. (가입된 이메일인 경우)' }; } // 기존 재설정 토큰 비활성화 await db .update(mfaTokens) .set({ isActive: false }) .where(and( eq(mfaTokens.userId, user[0].id) , eq(mfaTokens.type, 'password_reset') , eq(mfaTokens.isActive, true)) ); // 새 토큰 생성 (32바이트 랜덤) const resetToken = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + 1); // 1시간 후 만료 await db.insert(mfaTokens).values({ userId: user[0].id, token: resetToken, type: 'password_reset', expiresAt, isActive: true, }); // 재설정 링크 생성 const headersList = await headers(); const host = headersList.get('host') || 'localhost:3000'; // 로그인 또는 서명 페이지 URL 생성 // const baseUrl = `http://${host}` const baseUrl = process.env.NEXT_PUBLIC_DMZ_URL || `https://partners.sevcp.com` const resetLink = `${baseUrl}/auth/reset-password?token=${resetToken}`; // 이메일 전송 await sendEmail({ to: user[0].email, subject: user[0].language === 'ko' ? '비밀번호 재설정 요청' : 'Password Reset Request', template: 'password-reset', context: { language: user[0].language || 'ko', userName: user[0].name, resetLink: resetLink, expiryTime: '1시간', supportEmail: process.env.SUPPORT_EMAIL || 'support@evcp.com', }, }); return { success: true, message: '비밀번호 재설정 링크를 이메일로 전송했습니다.' }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, error: error.errors[0]?.message || '입력값이 올바르지 않습니다' }; } console.error('Password reset request error:', error); return { success: false, error: '재설정 요청 처리 중 오류가 발생했습니다' }; } } // 비밀번호 재설정 실행 (서버 액션) export async function resetPasswordAction( prevState: any, formData: FormData ): Promise<{ success: boolean; error?: string; message?: string }> { try { const rawData = { token: formData.get('token') as string, newPassword: formData.get('newPassword') as string, confirmPassword: formData.get('confirmPassword') as string, }; const validatedData = passwordResetSchema.parse(rawData); // 토큰 검증 const resetToken = await db .select({ id: mfaTokens.id, userId: mfaTokens.userId, expiresAt: mfaTokens.expiresAt, }) .from(mfaTokens) .where(and( eq(mfaTokens.token, validatedData.token) , eq(mfaTokens.type, 'password_reset') , eq(mfaTokens.isActive, true) ) ) .limit(1); if (!resetToken[0]) { return { success: false, error: '유효하지 않은 재설정 링크입니다' }; } if (resetToken[0].expiresAt < new Date()) { // 만료된 토큰 비활성화 await db .update(mfaTokens) .set({ isActive: false }) .where(eq(mfaTokens.id, resetToken[0].id)); return { success: false, error: '재설정 링크가 만료되었습니다. 다시 요청해주세요' }; } // 패스워드 변경 (기존 setPassword 함수 사용) const { setPassword } = await import('@/lib/users/auth/passwordUtil'); const result = await setPassword(resetToken[0].userId, validatedData.newPassword); if (result.success) { // 토큰 비활성화 await db .update(mfaTokens) .set({ isActive: false, usedAt: new Date(), }) .where(eq(mfaTokens.id, resetToken[0].id)); // 계정 잠금 해제 (패스워드 재설정 시) await db .update(users) .set({ isLocked: false, lockoutUntil: null, failedLoginAttempts: 0, }) .where(eq(users.id, resetToken[0].userId)); return { success: true, message: '비밀번호가 성공적으로 변경되었습니다. 새 비밀번호로 로그인해주세요.' }; } return { success: false, error: result.errors.join(', ') }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, error: error.errors[0]?.message || '입력값이 올바르지 않습니다' }; } console.error('Password reset error:', error); return { success: false, error: '비밀번호 재설정 중 오류가 발생했습니다' }; } } // 토큰 유효성 검증 (페이지 접근 시) export async function validateResetTokenAction( token: string ): Promise<{ valid: boolean; error?: string; expired?: boolean, userId?: number }> { try { if (!token) { return { valid: false, error: '토큰이 제공되지 않았습니다' }; } const resetToken = await db .select({ id: mfaTokens.id, expiresAt: mfaTokens.expiresAt, token: mfaTokens.token, isActive: mfaTokens.isActive, userId: mfaTokens.userId, }) .from(mfaTokens) .where( and(eq(mfaTokens.token, token), eq(mfaTokens.type, 'password_reset') ) ) .limit(1); console.log(token) console.log(resetToken[0], "resetToken") if (!resetToken[0]) { return { valid: false, error: '유효하지 않은 토큰입니다' }; } if (!resetToken[0].isActive) { return { valid: false, error: '이미 사용된 토큰입니다' }; } if (resetToken[0].expiresAt < new Date()) { return { valid: false, expired: true, error: '토큰이 만료되었습니다' }; } return { valid: true, userId: resetToken[0].userId }; } catch (error) { console.error('Token validation error:', error); return { valid: false, error: '토큰 검증 중 오류가 발생했습니다' }; } } export async function validatePasswordAction( password: string, userId?: number ): Promise { try { // 패스워드 강도 분석 const strength = analyzePasswordStrength(password); // 정책 검증 const policyResult = await validatePasswordPolicy(password); // 히스토리 검증 (userId가 있는 경우에만) let historyValid: boolean | undefined = undefined; if (userId) { historyValid = await checkPasswordHistory(userId, password); } return { strength, policyValid: policyResult.valid, policyErrors: policyResult.errors, historyValid, }; } catch (error) { console.error('Password validation error:', error); // 에러 발생 시 기본값 반환 return { strength: { score: 1, hasUppercase: false, hasLowercase: false, hasNumbers: false, hasSymbols: false, length: 0, feedback: ['검증 중 오류가 발생했습니다'], }, policyValid: false, policyErrors: ['검증 중 오류가 발생했습니다'], }; } } // 비활성 유저 탐지 export async function findInactiveUsers(inactiveDays: number = 90) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - inactiveDays); return await db .select({ id: users.id, email: users.email, name: users.name, lastLoginAt: users.lastLoginAt, }) .from(users) .where( and( eq(users.isActive, true), or( lt(users.lastLoginAt, cutoffDate), isNull(users.lastLoginAt) // 한번도 로그인하지 않은 유저 ) ) ); } // 유저 비활성화 (Soft Delete) export async function deactivateUser( userId: number, reason: 'INACTIVE' | 'ADMIN' | 'GDPR' = 'INACTIVE' ) { return await db .update(users) .set({ isActive: false, deactivatedAt: new Date(), deactivationReason: reason, }) .where(eq(users.id, userId)); } // 배치 비활성화 export async function deactivateInactiveUsers(inactiveDays: number = 90) { const inactiveUsers = await findInactiveUsers(inactiveDays); if (inactiveUsers.length === 0) { return { deactivatedCount: 0, users: [] }; } // 로그 기록 console.log(`Deactivating ${inactiveUsers.length} inactive users`); await db .update(users) .set({ isActive: false, deactivatedAt: new Date(), deactivationReason: 'INACTIVE', }) .where( inArray(users.id, inactiveUsers.map(u => u.id)) ); return { deactivatedCount: inactiveUsers.length, users: inactiveUsers }; }