From 1e46c2f3523f0f73a7ed378e9281dec24b23f8f8 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 23 Jun 2025 12:56:54 +0000 Subject: (김준회) SAML 2.0 relay-state 처리 및 redirect 상태코드 문제 디버깅 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/login/login-form copy 2.tsx | 470 --------------------------------- components/login/login-form copy.tsx | 468 -------------------------------- components/login/saml-login-button.tsx | 8 +- 3 files changed, 7 insertions(+), 939 deletions(-) delete mode 100644 components/login/login-form copy 2.tsx delete mode 100644 components/login/login-form copy.tsx (limited to 'components') diff --git a/components/login/login-form copy 2.tsx b/components/login/login-form copy 2.tsx deleted file mode 100644 index d5ac01b9..00000000 --- a/components/login/login-form copy 2.tsx +++ /dev/null @@ -1,470 +0,0 @@ -'use client'; - -import { useState, useEffect } from "react"; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { SendIcon, Loader2, GlobeIcon, ChevronDownIcon, Ship, InfoIcon, ArrowRight } from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; -import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem } from "@/components/ui/dropdown-menu" -import { useTranslation } from '@/i18n/client' -import { useRouter, useParams, usePathname, useSearchParams } from 'next/navigation'; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot, -} from "@/components/ui/input-otp" -import { signIn } from 'next-auth/react'; -import { sendOtpAction } from "@/lib/users/send-otp"; -import { verifyTokenAction } from "@/lib/users/verifyToken"; -import { buttonVariants } from "@/components/ui/button" -import Link from "next/link" -import Image from 'next/image'; -import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; - -export function LoginForm({ - className, - ...props -}: React.ComponentProps<"div">) { - - const params = useParams() || {}; - const pathname = usePathname() || ''; - const router = useRouter(); - const searchParams = useSearchParams(); - const token = searchParams?.get('token') || null; - const [showCredentialsForm, setShowCredentialsForm] = useState(false); - // 새로운 상태: 업체 등록 안내 표시 여부 - const [showVendorRegistrationInfo, setShowVendorRegistrationInfo] = useState(false); - - const lng = params.lng as string; - const { t, i18n } = useTranslation(lng, 'login'); - - const { toast } = useToast(); - - const handleChangeLanguage = (lang: string) => { - const segments = pathname.split('/'); - segments[1] = lang; - router.push(segments.join('/')); - }; - - const currentLanguageText = i18n.language === 'ko' ? t('languages.korean') : t('languages.english'); - - const [email, setEmail] = useState(''); - const [otpSent, setOtpSent] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [otp, setOtp] = useState(''); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - - // 업체 등록 페이지로 이동하는 함수 - const goToVendorRegistration = () => { - router.push(`/${lng}/partners/repository`); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - try { - const result = await sendOtpAction(email, lng); - - if (result.success) { - setOtpSent(true); - toast({ - title: t('otpSentTitle'), - description: t('otpSentMessage'), - }); - } else { - // Handle specific error types - let errorMessage = t('defaultErrorMessage'); - - // 업체 미등록 사용자 에러 처리 - if (result.error === 'userNotFound' || result.error === 'vendorNotRegistered') { - setShowVendorRegistrationInfo(true); - errorMessage = t('vendorNotRegistered') || '등록된 업체가 아닙니다. 먼저 업체 등록 신청을 해주세요.'; - } - - toast({ - title: t('errorTitle'), - description: result.message || errorMessage, - variant: 'destructive', - }); - } - } catch (error) { - // This will catch network errors or other unexpected issues - console.error(error); - toast({ - title: t('errorTitle'), - description: t('networkErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - }; - - async function handleOtpSubmit(e: React.FormEvent) { - e.preventDefault(); - setIsLoading(true); - - try { - // next-auth의 Credentials Provider로 로그인 시도 - const result = await signIn('credentials', { - email, - code: otp, - redirect: false, // 커스텀 처리 위해 redirect: false - }); - - if (result?.ok) { - // 토스트 메시지 표시 - toast({ - title: t('loginSuccess'), - description: t('youAreLoggedIn'), - }); - - router.push(`/${lng}/partners/dashboard`); - } else { - // 로그인 실패 시 에러 메시지에 업체 등록 관련 정보 포함 - if (result?.error === 'vendorNotRegistered') { - setShowVendorRegistrationInfo(true); - toast({ - title: t('errorTitle'), - description: t('vendorNotRegistered') || '등록된 업체가 아닙니다. 먼저 업체 등록 신청을 해주세요.', - variant: 'destructive', - }); - } else { - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } - } - } catch (error) { - console.error('Login error:', error); - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - } - - // 새로운 로그인 처리 함수 추가 - const handleCredentialsLogin = async () => { - if (!username || !password) { - toast({ - title: t('errorTitle'), - description: t('credentialsRequired'), - variant: 'destructive', - }); - return; - } - - setIsLoading(true); - - try { - // next-auth의 다른 credentials provider로 로그인 시도 - const result = await signIn('credentials-password', { - username, - password, - redirect: false, - }); - - if (result?.ok) { - toast({ - title: t('loginSuccess'), - description: t('youAreLoggedIn'), - }); - - router.push(`/${lng}/partners/dashboard`); - } else { - // 로그인 실패 시 업체 등록 관련 정보 표시 여부 결정 - if (result?.error === 'vendorNotRegistered') { - setShowVendorRegistrationInfo(true); - toast({ - title: t('errorTitle'), - description: t('vendorNotRegistered') || '등록된 업체가 아닙니다. 먼저 업체 등록 신청을 해주세요.', - variant: 'destructive', - }); - } else { - toast({ - title: t('errorTitle'), - description: t('invalidCredentials'), - variant: 'destructive', - }); - } - } - } catch (error) { - console.error('Login error:', error); - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - const verifyToken = async () => { - if (!token) return; - setIsLoading(true); - - try { - const data = await verifyTokenAction(token); - - if (data.valid) { - setOtpSent(true); - setEmail(data.email ?? ''); - } else { - toast({ - title: t('errorTitle'), - description: t('invalidToken'), - variant: 'destructive', - }); - } - } catch (error) { - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - }; - verifyToken(); - }, [token, toast, t]); - - return ( -
- {/* Left Content */} -
- {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */} -
-
- - eVCP -
- - {/* 업체 등록 신청 버튼 - 가시성 향상을 위해 variant 변경 */} - - - {t('registerVendor') || '업체 등록 신청'} - -
- - {/* Content section that occupies remaining space, centered vertically */} -
- {/* Your form container */} -
- {/* 업체 등록 안내 알림 - 특정 상황에서만 표시 */} - {showVendorRegistrationInfo && ( - - - - {t('vendorRegistrationRequired') || '업체 등록이 필요합니다'} - - - {t('vendorRegistrationMessage') || '로그인하시려면 먼저 업체 등록이 필요합니다. 아래 버튼을 클릭하여 등록을 진행해주세요.'} - - - - )} - -
-
-
-

{t('loginMessage')}

- - {/* 설명 텍스트 추가 - 업체 등록 관련 안내 */} -

- {t('loginDescription') || '등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'} -

-
- - {/* S-chips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */} - {!showCredentialsForm && ( - <> -
- setEmail(e.target.value)} - /> -
- - - {/* 구분선과 "Or continue with" 섹션 추가 */} -
-
- -
-
- - {t('orContinueWith')} - -
-
- - {/* S-chips 로그인 버튼 */} - - - {/* 업체 등록 안내 링크 추가 */} - - - )} - - {/* ID/비밀번호 로그인 폼 - 버튼 클릭 시에만 표시 */} - {showCredentialsForm && ( - <> -
- setUsername(e.target.value)} - /> - setPassword(e.target.value)} - /> - - - {/* 뒤로 가기 버튼 */} - - - {/* 업체 등록 안내 링크 추가 */} - -
- - )} - -
- - - - - - handleChangeLanguage(value)} - > - - {t('languages.english')} - - - {t('languages.korean')} - - - - -
-
-
- -
- {t('termsMessage')} {t('termsOfService')} {t('and')} - {t('privacyPolicy')}. -
-
-
-
- - {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */} -
- {/* Image 컴포넌트로 대체 */} -
- Background image -
-
-
-

“{t("blockquote")}”

- {/* */} -
-
-
-
- ) -} \ No newline at end of file diff --git a/components/login/login-form copy.tsx b/components/login/login-form copy.tsx deleted file mode 100644 index ef9eba10..00000000 --- a/components/login/login-form copy.tsx +++ /dev/null @@ -1,468 +0,0 @@ -'use client'; - -import { useState, useEffect } from "react"; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { SendIcon, Loader2, GlobeIcon, ChevronDownIcon, Ship, InfoIcon } from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; -import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem } from "@/components/ui/dropdown-menu" -import { useTranslation } from '@/i18n/client' -import { useRouter, useParams, usePathname, useSearchParams } from 'next/navigation'; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot, -} from "@/components/ui/input-otp" -import { signIn } from 'next-auth/react'; -import { sendOtpAction } from "@/lib/users/send-otp"; -import { verifyTokenAction } from "@/lib/users/verifyToken"; -import { buttonVariants } from "@/components/ui/button" -import Link from "next/link" -import Image from 'next/image'; // 추가: Image 컴포넌트 import - -export function LoginForm({ - className, - ...props -}: React.ComponentProps<"div">) { - - const params = useParams() || {}; - const pathname = usePathname() || ''; - const router = useRouter(); - const searchParams = useSearchParams(); - const token = searchParams?.get('token') || null; - const [showCredentialsForm, setShowCredentialsForm] = useState(false); - - - const lng = params.lng as string; - const { t, i18n } = useTranslation(lng, 'login'); - - const { toast } = useToast(); - - const handleChangeLanguage = (lang: string) => { - const segments = pathname.split('/'); - segments[1] = lang; - router.push(segments.join('/')); - }; - - const currentLanguageText = i18n.language === 'ko' ? t('languages.korean') : t('languages.english'); - - const [email, setEmail] = useState(''); - const [otpSent, setOtpSent] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [otp, setOtp] = useState(''); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - - const goToVendorRegistration = () => { - router.push(`/${lng}/partners/repository`); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - try { - const result = await sendOtpAction(email, lng); - - if (result.success) { - setOtpSent(true); - toast({ - title: t('otpSentTitle'), - description: t('otpSentMessage'), - }); - } else { - // Handle specific error types - let errorMessage = t('defaultErrorMessage'); - - // You can handle different error types differently - if (result.error === 'userNotFound') { - errorMessage = t('userNotFoundMessage'); - } - - toast({ - title: t('errorTitle'), - description: result.message || errorMessage, - variant: 'destructive', - }); - } - } catch (error) { - // This will catch network errors or other unexpected issues - console.error(error); - toast({ - title: t('errorTitle'), - description: t('networkErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - }; - - async function handleOtpSubmit(e: React.FormEvent) { - e.preventDefault(); - setIsLoading(true); - - try { - // next-auth의 Credentials Provider로 로그인 시도 - const result = await signIn('credentials', { - email, - code: otp, - redirect: false, // 커스텀 처리 위해 redirect: false - }); - - if (result?.ok) { - // 토스트 메시지 표시 - toast({ - title: t('loginSuccess'), - description: t('youAreLoggedIn'), - }); - - router.push(`/${lng}/partners/dashboard`); - - } else { - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } - } catch (error) { - console.error('Login error:', error); - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - } - - // 새로운 로그인 처리 함수 추가 - const handleCredentialsLogin = async () => { - if (!username || !password) { - toast({ - title: t('errorTitle'), - description: t('credentialsRequired'), - variant: 'destructive', - }); - return; - } - - setIsLoading(true); - - try { - // next-auth의 다른 credentials provider로 로그인 시도 - const result = await signIn('credentials-password', { - username, - password, - redirect: false, - }); - - if (result?.ok) { - toast({ - title: t('loginSuccess'), - description: t('youAreLoggedIn'), - }); - - router.push(`/${lng}/partners/dashboard`); - } else { - toast({ - title: t('errorTitle'), - description: t('invalidCredentials'), - variant: 'destructive', - }); - } - } catch (error) { - console.error('Login error:', error); - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - const verifyToken = async () => { - if (!token) return; - setIsLoading(true); - - try { - const data = await verifyTokenAction(token); - - if (data.valid) { - setOtpSent(true); - setEmail(data.email ?? ''); - } else { - toast({ - title: t('errorTitle'), - description: t('invalidToken'), - variant: 'destructive', - }); - } - } catch (error) { - toast({ - title: t('errorTitle'), - description: t('defaultErrorMessage'), - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - }; - verifyToken(); - }, [token, toast, t]); - - return ( -
- {/* Left Content */} -
- {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */} -
-
- {/* logo */} - - eVCP -
- - - {'업체 등록 신청'} - -
- - {/* Content section that occupies remaining space, centered vertically */} -
- {/* Your form container */} -
- - {/* Here's your existing login/OTP forms: */} - {/* {!otpSent ? ( */} - - {/*
*/} - -
-
-

{t('loginMessage')}

- - {/* 설명 텍스트 추가 - 업체 등록 관련 안내 */} -

- {'등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'} -

-
- - {/* S-Gips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */} - {!showCredentialsForm && ( - <> -
- setEmail(e.target.value)} - /> -
- - - {/* 구분선과 "Or continue with" 섹션 추가 */} -
-
- -
-
- - {t('orContinueWith')} - -
-
- - {/* S-Gips 로그인 버튼 */} - - - {/* 업체 등록 안내 링크 추가 */} - - - )} - - {/* ID/비밀번호 로그인 폼 - 버튼 클릭 시에만 표시 */} - {showCredentialsForm && ( - <> -
- setUsername(e.target.value)} - /> - setPassword(e.target.value)} - /> - - - {/* 뒤로 가기 버튼 */} - -
- - )} - -
- - - - - - handleChangeLanguage(value)} - > - - {t('languages.english')} - - - {t('languages.korean')} - - - - -
-
-
- {/* ) : ( -
-
-
-

{t('loginMessage')}

-
-
- setOtp(value)} - > - - - - - - - - - -
- -
- - - - - - handleChangeLanguage(value)} - > - - {t('languages.english')} - - - {t('languages.korean')} - - - - -
-
-
- )} */} - -
- {t('termsMessage')} {t('termsOfService')} {t('and')} - {t('privacyPolicy')}. -
-
-
-
- - {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */} -
- {/* Image 컴포넌트로 대체 */} -
- Background image -
-
-
-

“{t("blockquote")}”

- {/* */} -
-
-
-
- ) -} \ No newline at end of file diff --git a/components/login/saml-login-button.tsx b/components/login/saml-login-button.tsx index c0aae0f1..02825e2f 100644 --- a/components/login/saml-login-button.tsx +++ b/components/login/saml-login-button.tsx @@ -32,8 +32,14 @@ export function SAMLLoginButton({ try { setIsLoading(true) + // 현재 페이지 경로를 RelayState로 설정 (로그인 후 이 페이지로 돌아옴) + const currentPath = window.location.pathname + window.location.search + const relayState = encodeURIComponent(currentPath) + + console.log('Setting RelayState to:', currentPath) + // API 엔드포인트를 통해 SAML AuthnRequest URL 생성 - const response = await fetch('/api/auth/saml/authn-request', { + const response = await fetch(`/api/auth/saml/authn-request?relayState=${relayState}`, { method: 'GET', headers: { 'Content-Type': 'application/json', -- cgit v1.2.3