From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 21 Aug 2025 06:57:36 +0000 Subject: (대표님, 최겸) 설계메뉴추가, 작업사항 업데이트 설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/users/auth/passwordUtil.ts | 197 ++++++++++++++++++++++++++++------------- 1 file changed, 136 insertions(+), 61 deletions(-) (limited to 'lib/users/auth/passwordUtil.ts') diff --git a/lib/users/auth/passwordUtil.ts b/lib/users/auth/passwordUtil.ts index 54599761..4d342a61 100644 --- a/lib/users/auth/passwordUtil.ts +++ b/lib/users/auth/passwordUtil.ts @@ -12,6 +12,13 @@ import { mfaTokens } from '@/db/schema'; +// libphonenumber-js import 추가 +import { + parsePhoneNumber, + parsePhoneNumberFromString, + isValidPhoneNumber +} from 'libphonenumber-js'; + export interface PasswordStrength { score: number; // 1-5 hasUppercase: boolean; @@ -309,7 +316,62 @@ async function cleanupPasswordHistory(userId: number, keepCount: number) { } } -// MFA SMS 토큰 생성 및 전송 +// ========== SMS 관련 함수들 ========== + +// 전화번호에서 국가 정보 추출 (libphonenumber-js 사용) +function extractCountryInfo(phoneNumber: string): { + countryCode: string; + nationalNumber: string; + country?: string; +} | null { + try { + let parsed; + + // E.164 형식인지 확인 + if (phoneNumber.startsWith('+')) { + parsed = parsePhoneNumber(phoneNumber); + } else { + // 국가 코드가 없으면 한국으로 가정 (기본값) + parsed = parsePhoneNumberFromString(phoneNumber, 'KR'); + } + + if (!parsed || !isValidPhoneNumber(parsed.number)) { + return null; + } + + const countryCallingCode = parsed.countryCallingCode; + let nationalNumber = parsed.nationalNumber; + + // 국가별 특별 처리 + switch (countryCallingCode) { + case '82': // 한국 + // 한국 번호는 0으로 시작해야 함 + if (!nationalNumber.startsWith('0')) { + nationalNumber = '0' + nationalNumber; + } + break; + case '1': // 미국/캐나다 + // 이미 올바른 형식 + break; + case '81': // 일본 + // 이미 올바른 형식 + break; + case '86': // 중국 + // 이미 올바른 형식 + break; + } + + return { + countryCode: countryCallingCode, + nationalNumber: nationalNumber.replace(/[-\s]/g, ''), // 하이픈과 공백 제거 + country: parsed.country + }; + } catch (error) { + console.error('Country info extraction error:', error); + return null; + } +} + // Bizppurio API 토큰 발급 async function getBizppurioToken(): Promise { const account = process.env.BIZPPURIO_ACCOUNT; @@ -337,7 +399,7 @@ async function getBizppurioToken(): Promise { return data.accesstoken; } -// SMS 메시지 전송 +// SMS 메시지 전송 (libphonenumber-js 사용) async function sendSmsMessage(phoneNumber: string, message: string): Promise { try { const accessToken = await getBizppurioToken(); @@ -348,48 +410,32 @@ async function sendSmsMessage(phoneNumber: string, message: string): Promise { try { + // 전화번호 유효성 검사 + if (!isValidPhoneNumber(phoneNumber)) { + return { success: false, error: '유효하지 않은 전화번호입니다' }; + } + // 1. 일일 SMS 한도 체크 const settings = await db.select().from(securitySettings).limit(1); const maxSmsPerDay = settings[0]?.maxSmsAttemptsPerDay || 10; const today = new Date(); today.setHours(0, 0, 0, 0); - const tomorrow = new Date(today); - tomorrow.setDate(tomorrow.getDate() + 1); const todayCount = await db .select({ count: count() }) @@ -482,34 +566,24 @@ export async function generateAndSendSmsToken( const expiresAt = new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + expiryMinutes); + // 전화번호 정규화 (저장용) + const normalizedPhone = normalizePhoneNumber(phoneNumber); + if (!normalizedPhone) { + return { success: false, error: '전화번호 형식이 올바르지 않습니다' }; + } + await db.insert(mfaTokens).values({ userId, token, type: 'sms', - phoneNumber, + phoneNumber: normalizedPhone, // 정규화된 번호로 저장 expiresAt, isActive: true, }); - - let country = ''; - - if (phoneNumber.startsWith('+82')) { - country = '82'; - } else if (phoneNumber.startsWith('+1')) { - country = '1'; - } else if (phoneNumber.startsWith('+81')) { - country = '81'; - } else if (phoneNumber.startsWith('+86')) { - country = '86'; - } - // 국가코드가 없는 경우 한국으로 가정 - else if (!phoneNumber.startsWith('+')) { - country = '82'; - } - // 4. SMS 전송 (Bizppurio API 사용) - const message = getSmsMessage(country, token); - const smsResult = await sendSmsMessage(phoneNumber, message); + // 4. SMS 전송 + const message = getSmsMessage(normalizedPhone, token); + const smsResult = await sendSmsMessage(normalizedPhone, message); if (!smsResult) { // SMS 전송 실패 시 토큰 비활성화 @@ -526,7 +600,7 @@ export async function generateAndSendSmsToken( return { success: false, error: 'SMS 전송에 실패했습니다' }; } - console.log(`SMS 토큰 ${token}을 ${phoneNumber}로 전송했습니다`); + console.log(`SMS 토큰을 ${normalizedPhone}로 전송했습니다`); return { success: true }; } catch (error) { @@ -534,6 +608,7 @@ export async function generateAndSendSmsToken( return { success: false, error: 'SMS 전송 중 오류가 발생했습니다' }; } } + // SMS 토큰 검증 export async function verifySmsToken( userId: number, -- cgit v1.2.3