summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/login/login-form.tsx101
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>