'use server'; import { eq, and, sql } from 'drizzle-orm'; import db from '@/db/db'; import { users, mfaTokens } from '@/db/schema'; import { isEmailWhitelisted } from '@/lib/email-whitelist/service'; import { SessionRepository } from '@/lib/users/session/repository'; import { sendEmail } from '@/lib/mail/sendEmail'; /** * 이메일 화이트리스트 확인 및 인증 시작 * 화이트리스트면 바로 Email MFA 시작, 아니면 패스워드 필요 */ export async function checkEmailAndStartAuth(email: string): Promise<{ success: boolean; isWhitelisted: boolean; requiresPassword: boolean; userId?: number; userName?: string; tempAuthKey?: string; error?: string; errorCode?: string; }> { try { if (!email) { return { success: false, isWhitelisted: false, requiresPassword: false, error: '이메일을 입력해주세요.', errorCode: 'INVALID_INPUT' }; } const normalizedEmail = email.toLowerCase().trim(); // 1. 사용자 존재 확인 (대소문자 구분 없이 비교 - 기존 데이터 호환성) const [user] = await db .select({ id: users.id, name: users.name, email: users.email, language: users.language, isActive: users.isActive, isLocked: users.isLocked, lockoutUntil: users.lockoutUntil, }) .from(users) .where(sql`LOWER(${users.email}) = LOWER(${normalizedEmail})`) .limit(1); if (!user) { return { success: false, isWhitelisted: false, requiresPassword: false, error: '등록되지 않은 이메일입니다.', errorCode: 'USER_NOT_FOUND' }; } // 2. 계정 상태 확인 if (!user.isActive) { return { success: false, isWhitelisted: false, requiresPassword: false, error: '비활성화된 계정입니다.', errorCode: 'ACCOUNT_DEACTIVATED' }; } if (user.isLocked) { const now = new Date(); if (user.lockoutUntil && user.lockoutUntil > now) { const remainingMinutes = Math.ceil((user.lockoutUntil.getTime() - now.getTime()) / 60000); return { success: false, isWhitelisted: false, requiresPassword: false, error: `계정이 잠겼습니다. ${remainingMinutes}분 후 다시 시도해주세요.`, errorCode: 'ACCOUNT_LOCKED' }; } } // 3. 이메일 화이트리스트 확인 const whitelisted = await isEmailWhitelisted(normalizedEmail); if (whitelisted) { // 화이트리스트 이메일: 바로 Email MFA 시작 const expiresAt = new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + 10); // 10분 유효 // 임시 인증 세션 생성 (tempAuthSessions 테이블 사용) const tempAuthKey = await SessionRepository.createTempAuthSession({ userId: user.id, email: user.email, authMethod: 'email', expiresAt, }); // Email OTP 생성 및 전송 const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); const otpExpiresAt = new Date(); otpExpiresAt.setMinutes(otpExpiresAt.getMinutes() + 5); // 5분 유효 // 기존 OTP 비활성화 await db .update(mfaTokens) .set({ isActive: false }) .where( and( eq(mfaTokens.userId, user.id), eq(mfaTokens.type, 'email_otp'), eq(mfaTokens.isActive, true) ) ); // 새 OTP 생성 await db.insert(mfaTokens).values({ userId: user.id, token: otpCode, type: 'email_otp', expiresAt: otpExpiresAt, isActive: true, }); const userLanguage = user.language || 'en'; // OTP 이메일 전송 await sendEmail({ to: user.email, subject: '로그인 인증번호', template: 'otp', context: { language: userLanguage, name: user.name, otp: otpCode, verificationUrl: '', location: '', }, }); return { success: true, isWhitelisted: true, requiresPassword: false, userId: user.id, userName: user.name, tempAuthKey, }; } else { // 일반 이메일: 패스워드 입력 필요 return { success: true, isWhitelisted: false, requiresPassword: true, userId: user.id, userName: user.name, }; } } catch (error) { console.error('Email auth check error:', error); return { success: false, isWhitelisted: false, requiresPassword: false, error: '인증 처리 중 오류가 발생했습니다.', errorCode: 'SYSTEM_ERROR' }; } } /** * Email OTP 재전송 */ export async function resendEmailOtp(userId: number, email: string, userName: string): Promise<{ success: boolean; error?: string; }> { try { // OTP 생성 const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); const otpExpiresAt = new Date(); otpExpiresAt.setMinutes(otpExpiresAt.getMinutes() + 5); // 기존 OTP 비활성화 await db .update(mfaTokens) .set({ isActive: false }) .where( and( eq(mfaTokens.userId, userId), eq(mfaTokens.type, 'email_otp'), eq(mfaTokens.isActive, true) ) ); // 새 OTP 생성 await db.insert(mfaTokens).values({ userId, token: otpCode, type: 'email_otp', expiresAt: otpExpiresAt, isActive: true, }); // OTP 이메일 전송 await sendEmail({ to: email, subject: '로그인 인증번호', template: 'otp', context: { language: 'ko', name: userName, otp: otpCode, verificationUrl: '', location: '', }, }); return { success: true }; } catch (error) { console.error('Email OTP resend error:', error); return { success: false, error: '이메일 전송에 실패했습니다.', }; } }