diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-15 21:38:21 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-15 21:38:21 +0900 |
| commit | a070f833d132e6370311c0bbdad03beb51d595df (patch) | |
| tree | 9184292e4c2631ee0c7a7247f9728fc26de790f1 /components/login | |
| parent | 280a2628df810dc157357e0e4d2ed8076d020a2c (diff) | |
(김준회) 이메일 화이트리스트 (SMS 우회) 기능 추가 및 기존 로그인 과정 통합
Diffstat (limited to 'components/login')
| -rw-r--r-- | components/login/login-form.tsx | 101 |
1 files changed, 87 insertions, 14 deletions
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<number | null>(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() { ) : ( <> <div className="flex items-center justify-center w-12 h-12 rounded-full bg-blue-100 mb-4"> - 🔐 + {mfaType === 'email' ? '📧' : '🔐'} </div> - <h1 className="text-2xl font-bold">{t('smsVerification')}</h1> + <h1 className="text-2xl font-bold"> + {mfaType === 'email' ? '이메일 인증' : t('smsVerification')} + </h1> <p className="text-sm text-muted-foreground mt-2"> {selectedOtpUser ? t('firstAuthCompleteForSgips', { name: selectedOtpUser.name, email: mfaUserEmail }) - : t('firstAuthCompleteFor', { email: mfaUserEmail }) + : mfaType === 'email' + ? `${mfaUserEmail}로 인증번호가 전송되었습니다.` + : t('firstAuthCompleteFor', { email: mfaUserEmail }) } </p> <p className="text-xs text-muted-foreground mt-1"> - {t('enterSixDigitCodeInstructions')} + {mfaType === 'email' + ? '이메일에서 받은 6자리 인증번호를 입력해주세요.' + : t('enterSixDigitCodeInstructions')} </p> </> )} @@ -751,16 +816,24 @@ export function LoginForm() { </Button> </div> - {/* SMS 재전송 섹션 */} + {/* OTP 재전송 섹션 (SMS/Email) */} <div className="bg-gray-50 p-4 rounded-lg"> <h3 className="text-sm font-medium text-gray-900 mb-2"> {t('resendCode')} </h3> <p className="text-xs text-gray-600 mb-3"> - {t('didNotReceiveCode')} + {mfaType === 'email' + ? '이메일을 받지 못하셨나요?' + : t('didNotReceiveCode')} </p> <Button - onClick={() => handleSendSms()} + onClick={() => { + if (mfaType === 'email') { + handleSendEmail(); + } else { + handleSendSms(); + } + }} disabled={isSmsLoading || mfaCountdown > 0} variant="outline" size="sm" @@ -772,7 +845,7 @@ export function LoginForm() { ) : mfaCountdown > 0 ? ( t('resendAvailable', { seconds: mfaCountdown }) ) : ( - t('resendCode') + mfaType === 'email' ? '인증번호 재전송' : t('resendCode') )} </Button> </div> |
