summaryrefslogtreecommitdiff
path: root/lib/users
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-15 21:38:21 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-15 21:38:21 +0900
commita070f833d132e6370311c0bbdad03beb51d595df (patch)
tree9184292e4c2631ee0c7a7247f9728fc26de790f1 /lib/users
parent280a2628df810dc157357e0e4d2ed8076d020a2c (diff)
(김준회) 이메일 화이트리스트 (SMS 우회) 기능 추가 및 기존 로그인 과정 통합
Diffstat (limited to 'lib/users')
-rw-r--r--lib/users/auth/passwordUtil.ts146
-rw-r--r--lib/users/session/helper.ts16
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)