summaryrefslogtreecommitdiff
path: root/app/api/auth
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 /app/api/auth
parent280a2628df810dc157357e0e4d2ed8076d020a2c (diff)
(김준회) 이메일 화이트리스트 (SMS 우회) 기능 추가 및 기존 로그인 과정 통합
Diffstat (limited to 'app/api/auth')
-rw-r--r--app/api/auth/[...nextauth]/route.ts25
-rw-r--r--app/api/auth/first-auth/route.ts8
-rw-r--r--app/api/auth/send-email-otp/route.ts74
3 files changed, 98 insertions, 9 deletions
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
index 5896fb90..3b0f8c61 100644
--- a/app/api/auth/[...nextauth]/route.ts
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -11,7 +11,7 @@ import { getUserByEmail, getUserById } from '@/lib/users/repository'
import { authenticateWithSGips, verifyExternalCredentials } from '@/lib/users/auth/verifyCredentails'
import { verifyOtpTemp } from '@/lib/users/verifyOtp'
import { getSecuritySettings } from '@/lib/password-policy/service'
-import { verifySmsToken } from '@/lib/users/auth/passwordUtil'
+import { verifySmsToken, verifyEmailToken } from '@/lib/users/auth/passwordUtil'
import { SessionRepository } from '@/lib/users/session/repository'
import { getUserRoles } from '@/lib/users/service'
@@ -161,14 +161,15 @@ export const authOptions: NextAuthOptions = {
},
}),
- // ✅ MFA 완료 후 최종 인증 - roles 정보 추가
+ // ✅ MFA 완료 후 최종 인증 - roles 정보 추가 (SMS/Email OTP 지원)
CredentialsProvider({
id: 'credentials-mfa',
name: 'MFA Verification',
credentials: {
userId: { label: 'User ID', type: 'text' },
- smsToken: { label: 'SMS Token', type: 'text' },
+ smsToken: { label: 'SMS Token', type: 'text' }, // SMS 또는 Email OTP 토큰
tempAuthKey: { label: 'Temp Auth Key', type: 'text' },
+ mfaType: { label: 'MFA Type', type: 'text' }, // 'sms' 또는 'email'
},
async authorize(credentials, req) {
if (!credentials?.userId || !credentials?.smsToken || !credentials?.tempAuthKey) {
@@ -191,10 +192,20 @@ export const authOptions: NextAuthOptions = {
return null
}
- // SMS 토큰 검증
- const smsVerificationResult = await verifySmsToken(user.id, credentials.smsToken)
- if (!smsVerificationResult || !smsVerificationResult.success) {
- console.error('SMS token verification failed')
+ // MFA 타입에 따라 SMS 또는 Email OTP 검증
+ const mfaType = credentials.mfaType || 'sms'; // 기본값은 SMS
+ let verificationResult;
+
+ if (mfaType === 'email') {
+ verificationResult = await verifyEmailToken(user.id, credentials.smsToken)
+ console.log(`Email OTP verification for user ${user.email}:`, verificationResult.success)
+ } else {
+ verificationResult = await verifySmsToken(user.id, credentials.smsToken)
+ console.log(`SMS OTP verification for user ${user.email}:`, verificationResult.success)
+ }
+
+ if (!verificationResult || !verificationResult.success) {
+ console.error(`${mfaType.toUpperCase()} token verification failed`)
return null
}
diff --git a/app/api/auth/first-auth/route.ts b/app/api/auth/first-auth/route.ts
index 6952b472..93daf316 100644
--- a/app/api/auth/first-auth/route.ts
+++ b/app/api/auth/first-auth/route.ts
@@ -17,6 +17,8 @@ interface FirstAuthResponse {
tempAuthKey?: string
userId?: number
email?: string
+ mfaType?: 'sms' | 'email' // MFA 타입 추가
+ userName?: string // Email OTP 전송 시 필요
otpUsers?: Array<{
id: string
name: string
@@ -134,12 +136,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<FirstAuth
})
}
- // 일반 사용자의 경우 기존 응답
+ // 일반 사용자의 경우 mfaType 포함하여 응답
return NextResponse.json({
success: true,
tempAuthKey: authResult.tempAuthKey,
userId: authResult.userId,
- email: authResult.email
+ email: authResult.email,
+ mfaType: (authResult.mfaType || 'sms') as 'sms' | 'email', // 기본값은 SMS
+ userName: authResult.userName,
})
} catch (error) {
diff --git a/app/api/auth/send-email-otp/route.ts b/app/api/auth/send-email-otp/route.ts
new file mode 100644
index 00000000..92bdbe6d
--- /dev/null
+++ b/app/api/auth/send-email-otp/route.ts
@@ -0,0 +1,74 @@
+// app/api/auth/send-email-otp/route.ts
+// Email OTP 전송 API 엔드포인트
+
+import { NextRequest, NextResponse } from 'next/server';
+import { z } from 'zod';
+import { getUserById } from '@/lib/users/repository';
+import { generateAndSendEmailToken } from '@/lib/users/auth/passwordUtil';
+
+const sendEmailOtpSchema = z.object({
+ userId: z.number(),
+ email: z.string().email().optional(),
+ userName: z.string().optional(),
+});
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { userId, email, userName } = sendEmailOtpSchema.parse(body);
+
+ // 본인 확인
+ if (!userId) {
+ return NextResponse.json(
+ { error: '권한이 없습니다' },
+ { status: 403 }
+ );
+ }
+
+ // 사용자 정보 조회
+ const user = await getUserById(userId);
+ if (!user || !user.email) {
+ return NextResponse.json(
+ { error: '이메일 주소가 등록되지 않았습니다' },
+ { status: 400 }
+ );
+ }
+
+ // Email OTP 전송
+ const userEmail = email || user.email;
+ const userDisplayName = userName || user.name;
+
+ const result = await generateAndSendEmailToken(
+ Number(userId),
+ userEmail,
+ userDisplayName
+ );
+
+ if (result.success) {
+ return NextResponse.json({
+ success: true,
+ message: '이메일 인증번호가 전송되었습니다'
+ });
+ } else {
+ return NextResponse.json(
+ { error: result.error },
+ { status: 400 }
+ );
+ }
+
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: '잘못된 요청입니다' },
+ { status: 400 }
+ );
+ }
+
+ console.error('Email OTP send API error:', error);
+ return NextResponse.json(
+ { error: '서버 오류가 발생했습니다' },
+ { status: 500 }
+ );
+ }
+}
+