diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
| commit | 02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch) | |
| tree | e932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /lib/users | |
| parent | d78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff) | |
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리
설계메뉴 - 벤더 데이터
gtc 메뉴 업데이트
정보시스템 - 메뉴리스트 및 정보 업데이트
파일 라우트 업데이트
엑셀임포트 개선
기본계약 개선
벤더 가입과정 변경 및 개선
벤더 기본정보 - pq
돌체 오류 수정 및 개선
벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'lib/users')
| -rw-r--r-- | lib/users/auth/partners-auth.ts | 10 | ||||
| -rw-r--r-- | lib/users/auth/passwordUtil.ts | 197 |
2 files changed, 144 insertions, 63 deletions
diff --git a/lib/users/auth/partners-auth.ts b/lib/users/auth/partners-auth.ts index 5418e2a8..ac0dec08 100644 --- a/lib/users/auth/partners-auth.ts +++ b/lib/users/auth/partners-auth.ts @@ -8,7 +8,7 @@ import crypto from 'crypto'; import { PasswordStrength, passwordResetRequestSchema, passwordResetSchema } from './validataions-password'; import { sendEmail } from '@/lib/mail/sendEmail'; import { analyzePasswordStrength, checkPasswordHistory, validatePasswordPolicy } from '@/lib/users/auth/passwordUtil'; - +import { headers } from 'next/headers'; export interface PasswordValidationResult { strength: PasswordStrength; @@ -73,7 +73,13 @@ export async function requestPasswordResetAction( }); // 재설정 링크 생성 - const resetLink = `${process.env.NEXTAUTH_URL}/auth/reset-password?token=${resetToken}`; + const headersList = await headers(); + const host = headersList.get('host') || 'localhost:3000'; + // 로그인 또는 서명 페이지 URL 생성 + // const baseUrl = `http://${host}` + const baseUrl = process.env.NEXT_PUBLIC_DMZ_URL || `https://partners.sevcp.com` + + const resetLink = `${baseUrl}/auth/reset-password?token=${resetToken}`; // 이메일 전송 await sendEmail({ 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<string> { const account = process.env.BIZPPURIO_ACCOUNT; @@ -337,7 +399,7 @@ async function getBizppurioToken(): Promise<string> { return data.accesstoken; } -// SMS 메시지 전송 +// SMS 메시지 전송 (libphonenumber-js 사용) async function sendSmsMessage(phoneNumber: string, message: string): Promise<boolean> { try { const accessToken = await getBizppurioToken(); @@ -348,48 +410,32 @@ async function sendSmsMessage(phoneNumber: string, message: string): Promise<boo throw new Error('Bizppurio configuration missing'); } - // phoneNumber에서 국가코드와 번호 분리 - let country = ''; - let to = phoneNumber; - - if (phoneNumber.startsWith('+82')) { - country = '82'; - to = phoneNumber.substring(3); - // 한국 번호는 0으로 시작하지 않는 경우 0 추가 - if (!to.startsWith('0')) { - to = '0' + to; - } - } else if (phoneNumber.startsWith('+1')) { - country = '1'; - to = phoneNumber.substring(2); - } else if (phoneNumber.startsWith('+81')) { - country = '81'; - to = phoneNumber.substring(3); - } else if (phoneNumber.startsWith('+86')) { - country = '86'; - to = phoneNumber.substring(3); + // libphonenumber-js를 사용하여 전화번호 파싱 + const countryInfo = extractCountryInfo(phoneNumber); + + if (!countryInfo) { + throw new Error(`Invalid phone number format: ${phoneNumber}`); } - // 국가코드가 없는 경우 한국으로 가정 - else if (!phoneNumber.startsWith('+')) { - country = '82'; - to = phoneNumber.replace(/-/g, ''); // 하이픈 제거 - if (!to.startsWith('0')) { - to = '0' + to; - } + + console.log(`Sending SMS to ${phoneNumber}:`); + console.log(` Country Code: ${countryInfo.countryCode}`); + console.log(` National Number: ${countryInfo.nationalNumber}`); + if (countryInfo.country) { + console.log(` Country: ${countryInfo.country}`); } const requestBody = { account: account, type: 'sms', from: fromNumber, - to: to, - country: country, + to: countryInfo.nationalNumber, + country: countryInfo.countryCode, content: { sms: { message: message } }, - refkey: `sms_${Date.now()}_${Math.random().toString(36).substring(7)}` // 고객사에서 부여한 키 + refkey: `sms_${Date.now()}_${Math.random().toString(36).substring(7)}` }; const response = await fetch('https://api.bizppurio.com/v3/message', { @@ -409,7 +455,7 @@ async function sendSmsMessage(phoneNumber: string, message: string): Promise<boo const result = await response.json(); if (result.code === 1000) { - console.log(`SMS sent successfully. MessageKey: ${result.messagekey}`); + console.log(`SMS sent successfully to ${phoneNumber}. MessageKey: ${result.messagekey}`); return true; } else { throw new Error(`SMS send failed: ${result.description} (Code: ${result.code})`); @@ -420,7 +466,7 @@ async function sendSmsMessage(phoneNumber: string, message: string): Promise<boo } } - +// SMS 템플릿 (기존 유지) const SMS_TEMPLATES = { '82': '[인증번호] {token}', // 한국 '1': '[Verification Code] {token}', // 미국 @@ -429,25 +475,63 @@ const SMS_TEMPLATES = { 'default': '[Verification Code] {token}' // 기본값 (영어) } as const; -function getSmsMessage(country: string, token: string): string { - const template = SMS_TEMPLATES[country as keyof typeof SMS_TEMPLATES] || SMS_TEMPLATES.default; - return template.replace('{token}', token); +// SMS 메시지 생성 (libphonenumber-js 사용) +function getSmsMessage(phoneNumber: string, token: string): string { + try { + const countryInfo = extractCountryInfo(phoneNumber); + if (!countryInfo) { + return SMS_TEMPLATES.default.replace('{token}', token); + } + + const template = SMS_TEMPLATES[countryInfo.countryCode as keyof typeof SMS_TEMPLATES] || SMS_TEMPLATES.default; + return template.replace('{token}', token); + } catch (error) { + return SMS_TEMPLATES.default.replace('{token}', token); // 에러 시 기본값 + } +} + +// 전화번호 정규화 (저장용) +export function normalizePhoneNumber(phoneNumber: string, countryCode?: string): string | null { + try { + let parsed; + + if (countryCode) { + // 국가 코드가 제공된 경우 + parsed = parsePhoneNumberFromString(phoneNumber, countryCode); + } else { + // 국가 코드가 없는 경우 국제 형식으로 파싱 시도 + parsed = parsePhoneNumber(phoneNumber); + } + + if (!parsed || !isValidPhoneNumber(parsed.number)) { + return null; + } + + // 항상 국제 형식으로 반환 (예: +821012345678) + return parsed.format('E.164'); + } catch (error) { + console.error('Phone number normalization error:', error); + return null; + } } -// 업데이트된 메인 함수 +// SMS 토큰 생성 및 전송 (업데이트됨) export async function generateAndSendSmsToken( userId: number, phoneNumber: string ): Promise<{ success: boolean; error?: string }> { 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, |
