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 --- app/api/auth/[...nextauth]/saml/utils.ts | 19 +- app/api/auth/saml/authn-request/route.ts | 10 +- app/api/auth/saml/mock-idp/route.ts | 8 +- app/api/saml/callback/route.ts | 61 ++-- components/login/login-form copy 2.tsx | 470 ------------------------------- components/login/login-form copy.tsx | 468 ------------------------------ components/login/saml-login-button.tsx | 8 +- 7 files changed, 73 insertions(+), 971 deletions(-) delete mode 100644 components/login/login-form copy 2.tsx delete mode 100644 components/login/login-form copy.tsx diff --git a/app/api/auth/[...nextauth]/saml/utils.ts b/app/api/auth/[...nextauth]/saml/utils.ts index 73c00bf6..a5bcfe7a 100644 --- a/app/api/auth/[...nextauth]/saml/utils.ts +++ b/app/api/auth/[...nextauth]/saml/utils.ts @@ -97,15 +97,15 @@ export function createSAMLConfig() { } // SAML AuthnRequest 생성 (서버 액션) -export async function createAuthnRequest(): Promise { +export async function createAuthnRequest(relayState?: string): Promise { "use server"; - console.log("SSO STEP 2: Create AuthnRequest"); + console.log("SSO STEP 2: Create AuthnRequest", { relayState }); // Mock IdP 모드 체크 if (process.env.SAML_MOCKING_IDP === 'true') { debugMock("Mock IdP mode enabled - simulating SAML response"); - return createMockSAMLFlow(); + return createMockSAMLFlow(relayState); } try { @@ -117,7 +117,7 @@ export async function createAuthnRequest(): Promise { const startTime = Date.now(); const authorizeUrl = await saml.getAuthorizeUrlAsync( - "", // RelayState + relayState || "", // RelayState - 원래 가려던 페이지 undefined, // host { additionalParams: {}, @@ -406,12 +406,17 @@ export function mapSAMLProfileToUser(profile: SAMLProfile): SAMLUser { } // Mock SAML 플로우 생성 (테스트용) -function createMockSAMLFlow(): string { - debugMock("Creating mock SAML flow..."); +function createMockSAMLFlow(relayState?: string): string { + debugMock("Creating mock SAML flow...", { relayState }); // Mock 모드에서는 Mock IdP 엔드포인트로 리다이렉션 const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; - const mockIdpUrl = `${baseUrl}/api/auth/saml/mock-idp`; + let mockIdpUrl = `${baseUrl}/api/auth/saml/mock-idp`; + + // RelayState가 있으면 URL 파라미터로 전달 + if (relayState) { + mockIdpUrl += `?RelayState=${encodeURIComponent(relayState)}`; + } debugMock("Mock SAML Flow - redirecting to Mock IdP:", mockIdpUrl); diff --git a/app/api/auth/saml/authn-request/route.ts b/app/api/auth/saml/authn-request/route.ts index f079aea0..6544a765 100644 --- a/app/api/auth/saml/authn-request/route.ts +++ b/app/api/auth/saml/authn-request/route.ts @@ -50,17 +50,23 @@ function validateSAMLEnvironment() { * * @returns {JSON} { loginUrl: string, success: boolean, isThisMocking?: boolean } */ -export async function GET() { +export async function GET(request: Request) { debugProcess('🚀 SAML AuthnRequest API started') try { + // URL에서 RelayState 매개변수 추출 + const url = new URL(request.url) + const relayState = url.searchParams.get('relayState') + + debugLog('RelayState parameter:', relayState) + // 환경변수 검증 const environment = validateSAMLEnvironment() debugProcess('SSO STEP 1: Create AuthnRequest') const startTime = Date.now() - const loginUrl = await createAuthnRequest() + const loginUrl = await createAuthnRequest(relayState || undefined) const endTime = Date.now() debugSuccess('SAML AuthnRequest created successfully:', { diff --git a/app/api/auth/saml/mock-idp/route.ts b/app/api/auth/saml/mock-idp/route.ts index 45c670b0..eccb6035 100644 --- a/app/api/auth/saml/mock-idp/route.ts +++ b/app/api/auth/saml/mock-idp/route.ts @@ -3,7 +3,11 @@ import { NextRequest, NextResponse } from 'next/server' // Mock IdP 엔드포인트 - SAML Response HTML 폼 반환 export async function GET(request: NextRequest) { try { - console.log('🎭 Mock IdP endpoint accessed'); + // RelayState 파라미터 추출 + const url = new URL(request.url) + const relayState = url.searchParams.get('RelayState') || 'mock_test' + + console.log('🎭 Mock IdP endpoint accessed', { relayState }); // Mock SAML Response 데이터 (실제 형태와 일치하도록 문자열 형태) const mockSAMLResponseData = { @@ -83,7 +87,7 @@ export async function GET(request: NextRequest) {
- +
diff --git a/app/api/saml/callback/route.ts b/app/api/saml/callback/route.ts index cf9ea772..7f454cb9 100644 --- a/app/api/saml/callback/route.ts +++ b/app/api/saml/callback/route.ts @@ -12,18 +12,16 @@ import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-ut // GET 요청시 SP 메타데이터를 반환해주는데, 이건 필요 없으면 지우면 된다. export async function POST(request: NextRequest) { + // 안전한 baseUrl - 모든 리다이렉트에서 사용 + const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; + try { const isMockMode = process.env.SAML_MOCKING_IDP === 'true'; debugProcess(`SAML Callback received at /api/saml/callback ${isMockMode ? '(🎭 Mock Mode)' : ''}`) debugLog('Request info:', { - url: request.url, nextUrl: request.nextUrl?.toString(), mockMode: isMockMode, - headers: { - host: request.headers.get('host'), - origin: request.headers.get('origin'), - referer: request.headers.get('referer') - } + baseUrl: baseUrl }) // FormData에서 SAML Response 추출 @@ -75,10 +73,7 @@ export async function POST(request: NextRequest) { if (!samlResponse) { debugError('No SAML Response found in request') - const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000' - return NextResponse.redirect( - new URL('/ko/evcp', baseUrl) - ) + return NextResponse.redirect(new URL('/ko/evcp', baseUrl), 303) } // SAML Response 검증 및 파싱 @@ -99,10 +94,7 @@ export async function POST(request: NextRequest) { }) // SAML 검증 실패 시 evcp 페이지로 리다이렉트 - const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000' - return NextResponse.redirect( - new URL('/ko/evcp', baseUrl) - ) + return NextResponse.redirect(new URL('/ko/evcp', baseUrl), 303) } // SAML 프로필을 사용자 객체로 매핑 @@ -121,8 +113,7 @@ export async function POST(request: NextRequest) { if (!authenticatedUser) { debugError('SAML user authentication failed') - const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000' - return NextResponse.redirect(new URL('/ko/evcp', baseUrl)) + return NextResponse.redirect(new URL('/ko/evcp', baseUrl), 303) } debugSuccess('User authenticated successfully:', { @@ -137,7 +128,38 @@ export async function POST(request: NextRequest) { // NextAuth 세션 쿠키 설정 const cookieName = getSessionCookieName() - const response = NextResponse.redirect(new URL('/ko/evcp', request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000')) + // RelayState를 활용한 스마트 리다이렉트 + let redirectPath = '/ko/evcp' // 기본값 + + // RelayState 안전 처리 - null, 'null', undefined, 빈 문자열 모두 처리 + const isValidRelayState = relayState && + relayState !== 'null' && + relayState !== 'undefined' && + relayState.trim() !== '' && + typeof relayState === 'string'; + + if (isValidRelayState) { + debugLog('Using RelayState for redirect:', relayState) + // RelayState가 유효한 경로인지 확인 + if (relayState.startsWith('/') && !relayState.includes('//')) { + redirectPath = relayState + } else { + debugLog('Invalid RelayState format, using default:', relayState) + } + } else { + debugLog('No valid RelayState, using default path. RelayState value:', relayState) + } + + // URL 생성 전 최종 안전성 검사 + if (!redirectPath || typeof redirectPath !== 'string' || redirectPath.trim() === '') { + redirectPath = '/ko/evcp' // 안전한 기본값으로 재설정 + debugLog('redirectPath was invalid, reset to default:', redirectPath) + } + + debugLog('Final redirect path:', redirectPath) + + // POST 요청에 대한 응답으로는 303 See Other를 사용하여 GET으로 강제 변환 + const response = NextResponse.redirect(new URL(redirectPath, baseUrl), 303) response.cookies.set(cookieName, encodedToken, { httpOnly: true, @@ -153,10 +175,7 @@ export async function POST(request: NextRequest) { } catch (error) { debugError('SAML Callback processing failed:', error) - const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000' - return NextResponse.redirect( - new URL('/ko/evcp', baseUrl) - ) + return NextResponse.redirect(new URL('/ko/evcp', baseUrl), 303) } } 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")}”

- {/*
SHI
*/} -
-
-
-
- ) -} \ 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