diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-15 21:38:21 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-15 21:38:21 +0900 |
| commit | a070f833d132e6370311c0bbdad03beb51d595df (patch) | |
| tree | 9184292e4c2631ee0c7a7247f9728fc26de790f1 /lib/users | |
| parent | 280a2628df810dc157357e0e4d2ed8076d020a2c (diff) | |
(김준회) 이메일 화이트리스트 (SMS 우회) 기능 추가 및 기존 로그인 과정 통합
Diffstat (limited to 'lib/users')
| -rw-r--r-- | lib/users/auth/passwordUtil.ts | 146 | ||||
| -rw-r--r-- | lib/users/session/helper.ts | 16 |
2 files changed, 161 insertions, 1 deletions
diff --git a/lib/users/auth/passwordUtil.ts b/lib/users/auth/passwordUtil.ts index 0ff9e309..046e7d90 100644 --- a/lib/users/auth/passwordUtil.ts +++ b/lib/users/auth/passwordUtil.ts @@ -684,6 +684,152 @@ export async function verifySmsToken( } } +// Email OTP 생성 및 전송 +export async function generateAndSendEmailToken( + userId: number, + email: string, + userName: string +): Promise<{ success: boolean; error?: string }> { + try { + // 1. 보안 설정 가져오기 + const settings = await db.select().from(securitySettings).limit(1); + const expiryMinutes = settings[0]?.smsTokenExpiryMinutes || 10; // Email OTP도 동일한 만료 시간 사용 + + // 2. 일일 Email OTP 한도 체크 (30회) + const maxEmailPerDay = 30; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const todayCount = await db + .select({ count: count() }) + .from(mfaTokens) + .where( + and( + eq(mfaTokens.userId, userId), + eq(mfaTokens.type, 'email'), + gte(mfaTokens.createdAt, today) + ) + ); + + if (todayCount[0]?.count >= maxEmailPerDay) { + return { success: false, error: '일일 이메일 인증 한도를 초과했습니다' }; + } + + // 3. 이전 Email OTP 토큰 비활성화 + await db + .update(mfaTokens) + .set({ isActive: false }) + .where( + and( + eq(mfaTokens.userId, userId), + eq(mfaTokens.type, 'email'), + eq(mfaTokens.isActive, true) + ) + ); + + // 4. 새 토큰 생성 (6자리 숫자) + const token = Math.random().toString().slice(2, 8).padStart(6, '0'); + const expiresAt = new Date(); + expiresAt.setMinutes(expiresAt.getMinutes() + expiryMinutes); + + // 5. DB에 토큰 저장 + await db.insert(mfaTokens).values({ + userId, + token, + type: 'email', + expiresAt, + isActive: true, + attempts: 0, + }); + + // 6. 이메일 전송 + const { sendEmail } = await import('@/lib/mail/sendEmail'); + + await sendEmail({ + to: email, + template: 'otp', + context: { + name: userName, + otp: token, + verificationUrl: '', // Email OTP는 URL 없이 코드만 입력 + location: '', + language: 'ko', + }, + }); + + console.log(`Email OTP sent to user ${userId} (${email})`); + return { success: true }; + + } catch (error) { + console.error('Email OTP generation/sending error:', error); + return { success: false, error: '이메일 인증번호 전송 중 오류가 발생했습니다' }; + } +} + +// Email OTP 검증 +export async function verifyEmailToken( + userId: number, + token: string +): Promise<{ success: boolean; error?: string }> { + try { + + const mfaToken = await db + .select() + .from(mfaTokens) + .where( + and( + eq(mfaTokens.userId, userId), + eq(mfaTokens.token, token), + eq(mfaTokens.type, 'email'), + eq(mfaTokens.isActive, true) + ) + ) + .limit(1); + + if (!mfaToken[0]) { + return { success: false, error: '잘못된 인증번호입니다' }; + } + + // 만료 체크 + if (mfaToken[0].expiresAt < new Date()) { + await db + .update(mfaTokens) + .set({ isActive: false }) + .where(eq(mfaTokens.id, mfaToken[0].id)); + + return { success: false, error: '인증번호가 만료되었습니다' }; + } + + // 시도 횟수 증가 + const newAttempts = mfaToken[0].attempts + 1; + if (newAttempts > 3) { + await db + .update(mfaTokens) + .set({ isActive: false }) + .where(eq(mfaTokens.id, mfaToken[0].id)); + + return { success: false, error: '시도 횟수를 초과했습니다' }; + } + + // 토큰 사용 처리 + await db + .update(mfaTokens) + .set({ + usedAt: new Date(), + isActive: false, + attempts: newAttempts, + }) + .where(eq(mfaTokens.id, mfaToken[0].id)); + + return { success: true }; + + } catch (error) { + console.error('Email token verification error:', error); + return { success: false, error: '인증 중 오류가 발생했습니다' }; + } +} + // 패스워드 강제 변경 필요 체크 export async function checkPasswordChangeRequired(userId: number): Promise<boolean> { const user = await db diff --git a/lib/users/session/helper.ts b/lib/users/session/helper.ts index 03bfd7bc..4c511340 100644 --- a/lib/users/session/helper.ts +++ b/lib/users/session/helper.ts @@ -1,5 +1,6 @@ import { authenticateWithSGips, verifyExternalCredentials } from "../auth/verifyCredentails"; import { SessionRepository } from "./repository"; +import { isEmailWhitelisted } from "@/lib/email-whitelist/service"; // lib/session/helpers.ts - NextAuth 헬퍼 함수들 개선 export const authHelpers = { @@ -35,6 +36,16 @@ export const authHelpers = { return { success: false, error: 'INVALID_CREDENTIALS' } } + // 화이트리스트 체크하여 MFA 타입 결정 + const isWhitelisted = await isEmailWhitelisted(authResult.user.email); + const mfaType = isWhitelisted ? 'email' : 'sms'; + + console.log('Whitelist check:', { + email: authResult.user.email, + isWhitelisted, + mfaType + }); + // DB에 임시 인증 세션 생성 const expiresAt = new Date(Date.now() + (10 * 60 * 1000)) // 10분 후 만료 const tempAuthKey = await SessionRepository.createTempAuthSession({ @@ -49,6 +60,7 @@ export const authHelpers = { userId: authResult.user.id, email: authResult.user.email, authMethod: provider, + mfaType, expiresAt }) @@ -56,7 +68,9 @@ export const authHelpers = { success: true, tempAuthKey, userId: authResult.user.id, - email: authResult.user.email + email: authResult.user.email, + mfaType, // 'email' 또는 'sms' + userName: authResult.user.name, // Email OTP 전송 시 필요 } } catch (error) { console.error('First auth error:', error) |
