summaryrefslogtreecommitdiff
path: root/lib/users
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
commit02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch)
treee932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /lib/users
parentd78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff)
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'lib/users')
-rw-r--r--lib/users/auth/partners-auth.ts10
-rw-r--r--lib/users/auth/passwordUtil.ts197
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,