summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-16 18:11:48 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-16 18:11:48 +0900
commitc49d5a42a66d1d29d477cca2ad56f923313c3961 (patch)
tree5d243ae86daf172bd1817283f28d0b58d6c7c540 /components
parent5f29ee456fddfb85d1798094cf3e612f4b7647d8 (diff)
(김준회) 파트너 로그인 시, 이메일 화이트리스트 대상은 비밀번호 입력 안하도록 변경
Diffstat (limited to 'components')
-rw-r--r--components/login/login-form.tsx290
1 files changed, 213 insertions, 77 deletions
diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx
index d249b6b1..0e047cc7 100644
--- a/components/login/login-form.tsx
+++ b/components/login/login-form.tsx
@@ -20,6 +20,7 @@ import {
InputOTPSlot,
} from "@/components/ui/input-otp"
import { requestPasswordResetAction } from "@/lib/users/auth/partners-auth";
+import { checkEmailAndStartAuth, resendEmailOtp } from "@/lib/users/auth/email-auth";
import Loading from "../common/loading/loading";
type LoginMethod = 'username' | 'sgips';
@@ -59,6 +60,11 @@ export function LoginForm() {
const [mfaCountdown, setMfaCountdown] = useState(0);
const [mfaType, setMfaType] = useState<'sms' | 'email'>('sms'); // MFA 타입
+ // 이메일 기반 인증 상태
+ const [emailInput, setEmailInput] = useState('');
+ const [showPasswordInput, setShowPasswordInput] = useState(false);
+ const [isWhitelistedEmail, setIsWhitelistedEmail] = useState(false);
+
// 일반 로그인 폼 데이터
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
@@ -259,7 +265,7 @@ export function LoginForm() {
}
};
- // Email OTP 전송
+ // Email OTP 전송/재전송
const handleSendEmail = async (userIdParam?: number, emailParam?: string, userNameParam?: string) => {
const targetUserId = userIdParam || mfaUserId;
const targetEmail = emailParam || mfaUserEmail;
@@ -269,27 +275,19 @@ export function LoginForm() {
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,
- }),
- });
+ // 서버 액션 사용
+ const result = await resendEmailOtp(targetUserId, targetEmail, targetUserName);
- if (response.ok) {
+ if (result.success) {
setMfaCountdown(60);
toast({
title: '이메일 인증번호 전송',
description: `${targetEmail}로 인증번호가 전송되었습니다.`,
});
} else {
- const errorData = await response.json();
toast({
title: t('errorTitle'),
- description: errorData.error || '이메일 전송에 실패했습니다.',
+ description: result.error || '이메일 전송에 실패했습니다.',
variant: 'destructive',
});
}
@@ -393,14 +391,85 @@ export function LoginForm() {
}
};
- // 일반 사용자명/패스워드 1차 인증 처리
- const handleUsernameLogin = async (e: React.FormEvent) => {
+ // 이메일 제출 핸들러 (1단계)
+ const handleEmailSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- if (!username || !password) {
+ if (!emailInput) {
toast({
title: t('errorTitle'),
- description: t('credentialsRequired'),
+ description: '이메일을 입력해주세요.',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ setIsFirstAuthLoading(true);
+
+ try {
+ // 이메일 화이트리스트 확인 및 인증 시작
+ const result = await checkEmailAndStartAuth(emailInput);
+
+ if (!result.success) {
+ const errorMessage = getErrorMessage(
+ { errorCode: result.errorCode, message: result.error },
+ 'email'
+ );
+
+ toast({
+ title: t('errorTitle'),
+ description: errorMessage,
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ if (result.isWhitelisted) {
+ // 화이트리스트 이메일: 바로 Email MFA로 이동
+ setIsWhitelistedEmail(true);
+ setMfaUserId(result.userId!);
+ setMfaUserEmail(emailInput);
+ setMfaUserName(result.userName || '');
+ setTempAuthKey(result.tempAuthKey!);
+ setMfaType('email');
+ setShowMfaForm(true);
+
+ toast({
+ title: '이메일 인증',
+ description: `${emailInput}로 인증번호가 전송되었습니다.`,
+ });
+ } else {
+ // 일반 이메일: 패스워드 입력 필요
+ setIsWhitelistedEmail(false);
+ setUsername(emailInput);
+ setShowPasswordInput(true);
+
+ toast({
+ title: '패스워드 입력',
+ description: '패스워드를 입력해주세요.',
+ });
+ }
+ } catch (error: unknown) {
+ console.error('Email submit error:', error);
+
+ toast({
+ title: t('errorTitle'),
+ description: '이메일 확인 중 오류가 발생했습니다.',
+ variant: 'destructive',
+ });
+ } finally {
+ setIsFirstAuthLoading(false);
+ }
+ };
+
+ // 패스워드 제출 핸들러 (2단계 - 화이트리스트 아닌 경우)
+ const handlePasswordSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!password) {
+ toast({
+ title: t('errorTitle'),
+ description: '패스워드를 입력해주세요.',
variant: 'destructive',
});
return;
@@ -409,15 +478,14 @@ export function LoginForm() {
setIsFirstAuthLoading(true);
try {
- // 1차 인증만 수행 (세션 생성 안함)
+ // 1차 인증 수행 (패스워드 검증)
const authResult = await performFirstAuth(username, password, 'email');
if (authResult.success) {
- const userMfaType = authResult.mfaType || 'sms';
-
+ // 패스워드 인증 성공 -> SMS MFA로 진행
toast({
title: t('firstAuthComplete'),
- description: userMfaType === 'email' ? '이메일 인증을 진행합니다.' : t('proceedingSmsAuth'),
+ description: t('proceedingSmsAuth'),
});
// MFA 화면으로 전환
@@ -425,27 +493,26 @@ export function LoginForm() {
setMfaUserId(authResult.userId);
setMfaUserEmail(authResult.email);
setMfaUserName(authResult.userName || '');
- setMfaType(userMfaType);
+ setMfaType('sms');
setShowMfaForm(true);
- // MFA 타입에 따라 자동으로 OTP 전송
+ // SMS 자동 전송
setTimeout(() => {
- if (userMfaType === 'email') {
- handleSendEmail(authResult.userId, authResult.email, authResult.userName);
- } else {
- handleSendSms(authResult.userId);
- }
+ handleSendSms(authResult.userId);
}, 500);
toast({
- title: userMfaType === 'email' ? '이메일 인증 필요' : t('smsAuthRequired'),
- description: userMfaType === 'email' ? '이메일로 인증번호를 전송하고 있습니다.' : t('sendingCodeToPhone'),
+ title: t('smsAuthRequired'),
+ description: t('sendingCodeToPhone'),
});
}
} catch (error: unknown) {
- console.error('Username login error:', error);
-
- const errorMessage = getErrorMessage(error as { errorCode?: string; message?: string }, 'email');
+ console.error('Password submit error:', error);
+
+ const errorMessage = getErrorMessage(
+ error as { errorCode?: string; message?: string },
+ 'email'
+ );
toast({
title: t('errorTitle'),
@@ -576,16 +643,41 @@ export function LoginForm() {
}
};
- // MFA 화면에서 뒤로 가기
+ // MFA 화면 또는 패스워드 입력에서 뒤로 가기
const handleBackToLogin = () => {
- setShowMfaForm(false);
- setMfaToken('');
- setTempAuthKey('');
- setMfaUserId(null);
- setMfaUserEmail('');
- setMfaUserName('');
- setMfaType('sms'); // 기본값으로 초기화
- setMfaCountdown(0);
+ // MFA 화면인 경우
+ if (showMfaForm) {
+ // 화이트리스트 이메일인 경우 이메일 입력으로 돌아감
+ if (isWhitelistedEmail) {
+ setShowMfaForm(false);
+ setMfaToken('');
+ setTempAuthKey('');
+ setMfaUserId(null);
+ setMfaUserEmail('');
+ setMfaUserName('');
+ setMfaType('email');
+ setMfaCountdown(0);
+ setIsWhitelistedEmail(false);
+ } else {
+ // 일반 이메일인 경우 패스워드 입력으로 돌아감
+ setShowMfaForm(false);
+ setMfaToken('');
+ setTempAuthKey('');
+ setMfaUserId(null);
+ setMfaUserEmail('');
+ setMfaUserName('');
+ setMfaType('sms');
+ setMfaCountdown(0);
+ setShowPasswordInput(true);
+ }
+ } else if (showPasswordInput) {
+ // 패스워드 입력 화면인 경우 이메일 입력으로 돌아감
+ setShowPasswordInput(false);
+ setPassword('');
+ setUsername('');
+ }
+
+ // S-Gips 관련 초기화
setSelectedOtpUser(null);
setShowUserSelectionDialog(false);
setOtpUsers([]);
@@ -699,40 +791,82 @@ export function LoginForm() {
{/* Username Login Form */}
{loginMethod === 'username' && (
- <form onSubmit={handleUsernameLogin} className="grid gap-4">
- <div className="grid gap-2">
- <Input
- id="username"
- type="text"
- placeholder={t('emailPlaceholder')}
- required
- className="h-10"
- value={username}
- onChange={(e) => setUsername(e.target.value)}
- disabled={isFirstAuthLoading}
- />
- </div>
- <div className="grid gap-2">
- <Input
- id="password"
- type="password"
- placeholder={t('password')}
- required
- className="h-10"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- disabled={isFirstAuthLoading}
- />
- </div>
- <Button
- type="submit"
- className="w-full"
- variant="samsung"
- disabled={isFirstAuthLoading || !username || !password}
- >
- {isFirstAuthLoading ? t('authenticating') : t('login')}
- </Button>
- </form>
+ <>
+ {!showPasswordInput ? (
+ // 1단계: 이메일만 입력
+ <form onSubmit={handleEmailSubmit} className="grid gap-4">
+ <div className="grid gap-2">
+ <Input
+ id="email"
+ type="email"
+ placeholder={t('emailPlaceholder')}
+ required
+ className="h-10"
+ value={emailInput}
+ onChange={(e) => setEmailInput(e.target.value)}
+ disabled={isFirstAuthLoading}
+ autoFocus
+ />
+ </div>
+ <Button
+ type="submit"
+ className="w-full"
+ variant="samsung"
+ disabled={isFirstAuthLoading || !emailInput}
+ >
+ {isFirstAuthLoading ? t('authenticating') : '다음'}
+ </Button>
+ </form>
+ ) : (
+ // 2단계: 패스워드 입력
+ <div className="space-y-4">
+ {/* 뒤로 가기 버튼 */}
+ <div className="flex items-center justify-start">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={handleBackToLogin}
+ className="text-blue-600 hover:text-blue-800"
+ >
+ <ArrowLeft className="w-4 h-4 mr-1" />
+ {t('backToLogin')}
+ </Button>
+ </div>
+
+ {/* 이메일 표시 */}
+ <div className="bg-gray-50 p-3 rounded-lg">
+ <p className="text-sm text-gray-600 mb-1">로그인 이메일</p>
+ <p className="text-sm font-medium text-gray-900">{emailInput}</p>
+ </div>
+
+ {/* 패스워드 입력 폼 */}
+ <form onSubmit={handlePasswordSubmit} className="grid gap-4">
+ <div className="grid gap-2">
+ <Input
+ id="password"
+ type="password"
+ placeholder={t('password')}
+ required
+ className="h-10"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ disabled={isFirstAuthLoading}
+ autoFocus
+ />
+ </div>
+ <Button
+ type="submit"
+ className="w-full"
+ variant="samsung"
+ disabled={isFirstAuthLoading || !password}
+ >
+ {isFirstAuthLoading ? t('authenticating') : t('login')}
+ </Button>
+ </form>
+ </div>
+ )}
+ </>
)}
{/* S-Gips Login Form - 영문 페이지에서 비활성화 0925 구매 요청사항*/}
@@ -778,6 +912,7 @@ export function LoginForm() {
{/* Additional Links */}
<div className="flex flex-col gap-2 text-center">
+ {/* 신규 업체 등록은 항상 표시 */}
<Button
type="button"
variant="link"
@@ -787,7 +922,8 @@ export function LoginForm() {
{t('newVendor')}
</Button>
- {loginMethod === 'username' && (
+ {/* 비밀번호 찾기는 패스워드 입력 단계에서만 표시 */}
+ {loginMethod === 'username' && showPasswordInput && (
<Button
type="button"
variant="link"