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') || '로그인하시려면 먼저 업체 등록이 필요합니다. 아래 버튼을 클릭하여 등록을 진행해주세요.'}
-
-
-
- )}
-
-
-
-
-
-
-
-
- {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */}
-
- {/* 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) */}
-
-
- {/*

*/}
-
-
eVCP
-
-
-
- {'업체 등록 신청'}
-
-
-
- {/* Content section that occupies remaining space, centered vertically */}
-
- {/* Your form container */}
-
-
- {/* Here's your existing login/OTP forms: */}
- {/* {!otpSent ? ( */}
-
- {/*
- {/* ) : (
-
- )} */}
-
-
-
-
-
-
- {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */}
-
- {/* 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