From a070f833d132e6370311c0bbdad03beb51d595df Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 15 Oct 2025 21:38:21 +0900 Subject: (김준회) 이메일 화이트리스트 (SMS 우회) 기능 추가 및 기존 로그인 과정 통합 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/login/login-form.tsx | 101 ++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 14 deletions(-) (limited to 'components') diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index 090f3a70..d249b6b1 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -55,7 +55,9 @@ export function LoginForm() { const [tempAuthKey, setTempAuthKey] = useState(''); const [mfaUserId, setMfaUserId] = useState(null); const [mfaUserEmail, setMfaUserEmail] = useState(''); + const [mfaUserName, setMfaUserName] = useState(''); // Email OTP 전송 시 필요 const [mfaCountdown, setMfaCountdown] = useState(0); + const [mfaType, setMfaType] = useState<'sms' | 'email'>('sms'); // MFA 타입 // 일반 로그인 폼 데이터 const [username, setUsername] = useState(''); @@ -257,6 +259,52 @@ export function LoginForm() { } }; + // Email OTP 전송 + const handleSendEmail = async (userIdParam?: number, emailParam?: string, userNameParam?: string) => { + const targetUserId = userIdParam || mfaUserId; + const targetEmail = emailParam || mfaUserEmail; + const targetUserName = userNameParam || mfaUserName; + + if (!targetUserId || mfaCountdown > 0) return; + + setIsSmsLoading(true); + try { + const response = await fetch('/api/auth/send-email-otp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: targetUserId, + email: targetEmail, + userName: targetUserName, + }), + }); + + if (response.ok) { + setMfaCountdown(60); + toast({ + title: '이메일 인증번호 전송', + description: `${targetEmail}로 인증번호가 전송되었습니다.`, + }); + } else { + const errorData = await response.json(); + toast({ + title: t('errorTitle'), + description: errorData.error || '이메일 전송에 실패했습니다.', + variant: 'destructive', + }); + } + } catch (error) { + console.error('Email OTP send error:', error); + toast({ + title: t('errorTitle'), + description: '이메일 전송 중 오류가 발생했습니다.', + variant: 'destructive', + }); + } finally { + setIsSmsLoading(false); + } + }; + // MFA 토큰 검증 및 최종 로그인 const handleMfaSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -283,11 +331,12 @@ export function LoginForm() { setIsMfaLoading(true); try { - // NextAuth의 credentials-mfa 프로바이더로 최종 인증 + // NextAuth의 credentials-mfa 프로바이더로 최종 인증 (mfaType 포함) const result = await signIn('credentials-mfa', { userId: mfaUserId, smsToken: mfaToken, tempAuthKey: tempAuthKey, + mfaType: mfaType, // 'sms' 또는 'email' redirect: false, }); @@ -364,25 +413,33 @@ export function LoginForm() { const authResult = await performFirstAuth(username, password, 'email'); if (authResult.success) { + const userMfaType = authResult.mfaType || 'sms'; + toast({ title: t('firstAuthComplete'), - description: t('proceedingSmsAuth'), + description: userMfaType === 'email' ? '이메일 인증을 진행합니다.' : t('proceedingSmsAuth'), }); // MFA 화면으로 전환 setTempAuthKey(authResult.tempAuthKey); setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); + setMfaUserName(authResult.userName || ''); + setMfaType(userMfaType); setShowMfaForm(true); - // 자동으로 SMS 전송 (userId 직접 전달) + // MFA 타입에 따라 자동으로 OTP 전송 setTimeout(() => { - handleSendSms(authResult.userId); + if (userMfaType === 'email') { + handleSendEmail(authResult.userId, authResult.email, authResult.userName); + } else { + handleSendSms(authResult.userId); + } }, 500); toast({ - title: t('smsAuthRequired'), - description: t('sendingCodeToPhone'), + title: userMfaType === 'email' ? '이메일 인증 필요' : t('smsAuthRequired'), + description: userMfaType === 'email' ? '이메일로 인증번호를 전송하고 있습니다.' : t('sendingCodeToPhone'), }); } } catch (error: unknown) { @@ -526,6 +583,8 @@ export function LoginForm() { setTempAuthKey(''); setMfaUserId(null); setMfaUserEmail(''); + setMfaUserName(''); + setMfaType('sms'); // 기본값으로 초기화 setMfaCountdown(0); setSelectedOtpUser(null); setShowUserSelectionDialog(false); @@ -582,17 +641,23 @@ export function LoginForm() { ) : ( <>
- 🔐 + {mfaType === 'email' ? '📧' : '🔐'}
-

{t('smsVerification')}

+

+ {mfaType === 'email' ? '이메일 인증' : t('smsVerification')} +

{selectedOtpUser ? t('firstAuthCompleteForSgips', { name: selectedOtpUser.name, email: mfaUserEmail }) - : t('firstAuthCompleteFor', { email: mfaUserEmail }) + : mfaType === 'email' + ? `${mfaUserEmail}로 인증번호가 전송되었습니다.` + : t('firstAuthCompleteFor', { email: mfaUserEmail }) }

- {t('enterSixDigitCodeInstructions')} + {mfaType === 'email' + ? '이메일에서 받은 6자리 인증번호를 입력해주세요.' + : t('enterSixDigitCodeInstructions')}

)} @@ -751,16 +816,24 @@ export function LoginForm() { - {/* SMS 재전송 섹션 */} + {/* OTP 재전송 섹션 (SMS/Email) */}

{t('resendCode')}

- {t('didNotReceiveCode')} + {mfaType === 'email' + ? '이메일을 받지 못하셨나요?' + : t('didNotReceiveCode')}

-- cgit v1.2.3