diff options
Diffstat (limited to 'lib/users/auth')
| -rw-r--r-- | lib/users/auth/passwordUtil.ts | 146 |
1 files changed, 146 insertions, 0 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 |
