diff options
Diffstat (limited to 'lib/users')
| -rw-r--r-- | lib/users/auth/email-auth.ts | 229 | ||||
| -rw-r--r-- | lib/users/auth/passwordUtil.ts | 8 |
2 files changed, 233 insertions, 4 deletions
diff --git a/lib/users/auth/email-auth.ts b/lib/users/auth/email-auth.ts new file mode 100644 index 00000000..edd4a634 --- /dev/null +++ b/lib/users/auth/email-auth.ts @@ -0,0 +1,229 @@ +'use server'; + +import { eq, and } 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, + isActive: users.isActive, + isLocked: users.isLocked, + lockoutUntil: users.lockoutUntil, + }) + .from(users) + .where(eq(users.email, 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, + }); + + // OTP 이메일 전송 + await sendEmail({ + to: user.email, + subject: '로그인 인증번호', + template: 'otp', + context: { + language: 'ko', + 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: '이메일 전송에 실패했습니다.', + }; + } +} + diff --git a/lib/users/auth/passwordUtil.ts b/lib/users/auth/passwordUtil.ts index 046e7d90..4d5d69f6 100644 --- a/lib/users/auth/passwordUtil.ts +++ b/lib/users/auth/passwordUtil.ts @@ -707,7 +707,7 @@ export async function generateAndSendEmailToken( .where( and( eq(mfaTokens.userId, userId), - eq(mfaTokens.type, 'email'), + eq(mfaTokens.type, 'email_otp'), gte(mfaTokens.createdAt, today) ) ); @@ -723,7 +723,7 @@ export async function generateAndSendEmailToken( .where( and( eq(mfaTokens.userId, userId), - eq(mfaTokens.type, 'email'), + eq(mfaTokens.type, 'email_otp'), eq(mfaTokens.isActive, true) ) ); @@ -737,7 +737,7 @@ export async function generateAndSendEmailToken( await db.insert(mfaTokens).values({ userId, token, - type: 'email', + type: 'email_otp', expiresAt, isActive: true, attempts: 0, @@ -781,7 +781,7 @@ export async function verifyEmailToken( and( eq(mfaTokens.userId, userId), eq(mfaTokens.token, token), - eq(mfaTokens.type, 'email'), + eq(mfaTokens.type, 'email_otp'), eq(mfaTokens.isActive, true) ) ) |
