summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/api/auth/[...nextauth]/saml/utils.ts19
-rw-r--r--app/api/auth/saml/authn-request/route.ts10
-rw-r--r--app/api/auth/saml/mock-idp/route.ts8
-rw-r--r--app/api/saml/callback/route.ts61
-rw-r--r--components/login/login-form copy 2.tsx470
-rw-r--r--components/login/login-form copy.tsx468
-rw-r--r--components/login/saml-login-button.tsx8
7 files changed, 73 insertions, 971 deletions
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<string> {
+export async function createAuthnRequest(relayState?: string): Promise<string> {
"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<string> {
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) {
</div>
<form id="mockForm" method="POST" action="${callbackUrl}">
<input type="hidden" name="SAMLResponse" value="${encodedSAMLResponse}" />
- <input type="hidden" name="RelayState" value="mock_test" />
+ <input type="hidden" name="RelayState" value="${relayState}" />
<button type="submit" class="button">Continue with Mock Login</button>
</form>
<div class="details">
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 (
- <div className="container relative flex h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
- {/* Left Content */}
- <div className="flex flex-col w-full h-screen lg:p-2">
- {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */}
- <div className="flex items-center justify-between p-4">
- <div className="flex items-center space-x-2">
- <Ship className="w-4 h-4" />
- <span className="text-md font-bold">eVCP</span>
- </div>
-
- {/* 업체 등록 신청 버튼 - 가시성 향상을 위해 variant 변경 */}
- <Link
- href={`/${lng}/partners/repository`}
- className={cn(buttonVariants({ variant: "outline" }), "border-blue-500 text-blue-600 hover:bg-blue-50")}
- >
- <InfoIcon className="w-4 h-4 mr-2" />
- {t('registerVendor') || '업체 등록 신청'}
- </Link>
- </div>
-
- {/* Content section that occupies remaining space, centered vertically */}
- <div className="flex-1 flex items-center justify-center">
- {/* Your form container */}
- <div className="mx-auto w-full flex flex-col space-y-6 sm:w-[350px]">
- {/* 업체 등록 안내 알림 - 특정 상황에서만 표시 */}
- {showVendorRegistrationInfo && (
- <Alert className="border-blue-500 bg-blue-50">
- <InfoIcon className="h-4 w-4 text-blue-600" />
- <AlertTitle className="text-blue-700">
- {t('vendorRegistrationRequired') || '업체 등록이 필요합니다'}
- </AlertTitle>
- <AlertDescription className="text-blue-600">
- {t('vendorRegistrationMessage') || '로그인하시려면 먼저 업체 등록이 필요합니다. 아래 버튼을 클릭하여 등록을 진행해주세요.'}
- </AlertDescription>
- <Button
- onClick={goToVendorRegistration}
- className="mt-2 w-full bg-blue-600 hover:bg-blue-700 text-white"
- >
- {t('goToVendorRegistration') || '업체 등록 신청하기'}
- <ArrowRight className="ml-2 h-4 w-4" />
- </Button>
- </Alert>
- )}
-
- <form onSubmit={handleOtpSubmit} className="p-6 md:p-8">
- <div className="flex flex-col gap-6">
- <div className="flex flex-col items-center text-center">
- <h1 className="text-2xl font-bold">{t('loginMessage')}</h1>
-
- {/* 설명 텍스트 추가 - 업체 등록 관련 안내 */}
- <p className="text-sm text-muted-foreground mt-2">
- {t('loginDescription') || '등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'}
- </p>
- </div>
-
- {/* S-chips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */}
- {!showCredentialsForm && (
- <>
- <div className="grid gap-2">
- <Input
- id="email"
- type="email"
- placeholder={t('email')}
- required
- className="h-10"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- />
- </div>
- <Button
- type="submit"
- className="w-full"
- variant="samsung"
- disabled={isLoading}
- onClick={handleSubmit}
- >
- {isLoading ? t('sending') : t('ContinueWithEmail')}
- </Button>
-
- {/* 구분선과 "Or continue with" 섹션 추가 */}
- <div className="relative">
- <div className="absolute inset-0 flex items-center">
- <span className="w-full border-t"></span>
- </div>
- <div className="relative flex justify-center text-xs uppercase">
- <span className="bg-background px-2 text-muted-foreground">
- {t('orContinueWith')}
- </span>
- </div>
- </div>
-
- {/* S-chips 로그인 버튼 */}
- <Button
- type="button"
- className="w-full"
- onClick={() => setShowCredentialsForm(true)}
- >
- S-Gips로 로그인하기
- </Button>
-
- {/* 업체 등록 안내 링크 추가 */}
- <Button
- type="button"
- variant="link"
- className="text-blue-600 hover:text-blue-800"
- onClick={goToVendorRegistration}
- >
- {t('newVendor') || '신규 업체이신가요? 여기서 등록하세요'}
- </Button>
- </>
- )}
-
- {/* ID/비밀번호 로그인 폼 - 버튼 클릭 시에만 표시 */}
- {showCredentialsForm && (
- <>
- <div className="grid gap-4">
- <Input
- id="username"
- type="text"
- placeholder="S-chips ID"
- className="h-10"
- value={username}
- onChange={(e) => setUsername(e.target.value)}
- />
- <Input
- id="password"
- type="password"
- placeholder="비밀번호"
- className="h-10"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- />
- <Button
- type="button"
- className="w-full"
- variant="samsung"
- onClick={handleCredentialsLogin}
- disabled={isLoading}
- >
- {isLoading ? "로그인 중..." : "로그인"}
- </Button>
-
- {/* 뒤로 가기 버튼 */}
- <Button
- type="button"
- variant="ghost"
- className="w-full text-sm"
- onClick={() => setShowCredentialsForm(false)}
- >
- 이메일로 로그인하기
- </Button>
-
- {/* 업체 등록 안내 링크 추가 */}
- <Button
- type="button"
- variant="link"
- className="text-blue-600 hover:text-blue-800"
- onClick={goToVendorRegistration}
- >
- {t('newVendor') || '신규 업체이신가요? 여기서 등록하세요'}
- </Button>
- </div>
- </>
- )}
-
- <div className="text-center text-sm mx-auto">
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" className="flex items-center gap-2">
- <GlobeIcon className="h-4 w-4" />
- <span>{currentLanguageText}</span>
- <ChevronDownIcon className="h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuRadioGroup
- value={i18n.language}
- onValueChange={(value) => handleChangeLanguage(value)}
- >
- <DropdownMenuRadioItem value="en">
- {t('languages.english')}
- </DropdownMenuRadioItem>
- <DropdownMenuRadioItem value="ko">
- {t('languages.korean')}
- </DropdownMenuRadioItem>
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- </div>
- </form>
-
- <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
- {t('termsMessage')} <a href="#">{t('termsOfService')}</a> {t('and')}
- <a href="#">{t('privacyPolicy')}</a>.
- </div>
- </div>
- </div>
- </div>
-
- {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */}
- <div className="relative hidden h-full flex-col p-10 text-white dark:border-r md:flex">
- {/* Image 컴포넌트로 대체 */}
- <div className="absolute inset-0">
- <Image
- src="/images/02.jpg"
- alt="Background image"
- fill
- priority
- sizes="(max-width: 1024px) 100vw, 50vw"
- className="object-cover"
- />
- </div>
- <div className="relative z-10 mt-auto">
- <blockquote className="space-y-2">
- <p className="text-sm">&ldquo;{t("blockquote")}&rdquo;</p>
- {/* <footer className="text-sm">SHI</footer> */}
- </blockquote>
- </div>
- </div>
- </div>
- )
-} \ 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 (
- <div className="container relative flex h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
- {/* Left Content */}
- <div className="flex flex-col w-full h-screen lg:p-2">
- {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */}
- <div className="flex items-center justify-between">
- <div className="flex items-center space-x-2">
- {/* <img
- src="/images/logo.png"
- alt="logo"
- className="h-8 w-auto"
- /> */}
- <Ship className="w-4 h-4" />
- <span className="text-md font-bold">eVCP</span>
- </div>
- <Link
- href="/partners/repository"
- className={cn(buttonVariants({ variant: "ghost" }))}
- >
- <InfoIcon className="w-4 h-4 mr-1" />
- {'업체 등록 신청'}
- </Link>
- </div>
-
- {/* Content section that occupies remaining space, centered vertically */}
- <div className="flex-1 flex items-center justify-center">
- {/* Your form container */}
- <div className="mx-auto w-full flex flex-col space-y-6 sm:w-[350px]">
-
- {/* Here's your existing login/OTP forms: */}
- {/* {!otpSent ? ( */}
-
- {/* <form onSubmit={handleSubmit} className="p-6 md:p-8"> */}
- <form onSubmit={handleOtpSubmit} className="p-6 md:p-8">
- <div className="flex flex-col gap-6">
- <div className="flex flex-col items-center text-center">
- <h1 className="text-2xl font-bold">{t('loginMessage')}</h1>
-
- {/* 설명 텍스트 추가 - 업체 등록 관련 안내 */}
- <p className="text-xs text-muted-foreground mt-2">
- {'등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'}
- </p>
- </div>
-
- {/* S-Gips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */}
- {!showCredentialsForm && (
- <>
- <div className="grid gap-2">
- <Input
- id="email"
- type="email"
- placeholder={t('email')}
- required
- className="h-10"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- />
- </div>
- <Button type="submit" className="w-full" variant="samsung" disabled={isLoading}>
- {isLoading ? t('sending') : t('ContinueWithEmail')}
- </Button>
-
- {/* 구분선과 "Or continue with" 섹션 추가 */}
- <div className="relative">
- <div className="absolute inset-0 flex items-center">
- <span className="w-full border-t"></span>
- </div>
- <div className="relative flex justify-center text-xs uppercase">
- <span className="bg-background px-2 text-muted-foreground">
- {t('orContinueWith')}
- </span>
- </div>
- </div>
-
- {/* S-Gips 로그인 버튼 */}
- <Button
- type="button"
- className="w-full"
- // variant=""
- onClick={() => setShowCredentialsForm(true)}
- >
- S-Gips로 로그인하기
- </Button>
-
- {/* 업체 등록 안내 링크 추가 */}
- <Button
- type="button"
- variant="link"
- className="text-blue-600 hover:text-blue-800"
- onClick={goToVendorRegistration}
- >
- {'신규 업체이신가요? 여기서 등록하세요'}
- </Button>
- </>
- )}
-
- {/* ID/비밀번호 로그인 폼 - 버튼 클릭 시에만 표시 */}
- {showCredentialsForm && (
- <>
- <div className="grid gap-4">
- <Input
- id="username"
- type="text"
- placeholder="S-Gips ID"
- className="h-10"
- value={username}
- onChange={(e) => setUsername(e.target.value)}
- />
- <Input
- id="password"
- type="password"
- placeholder="비밀번호"
- className="h-10"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- />
- <Button
- type="button"
- className="w-full"
- variant="samsung"
- onClick={handleCredentialsLogin}
- disabled={isLoading}
- >
- {isLoading ? "로그인 중..." : "로그인"}
- </Button>
-
- {/* 뒤로 가기 버튼 */}
- <Button
- type="button"
- variant="ghost"
- className="w-full text-sm"
- onClick={() => setShowCredentialsForm(false)}
- >
- 이메일로 로그인하기
- </Button>
- </div>
- </>
- )}
-
- <div className="text-center text-sm mx-auto">
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" className="flex items-center gap-2">
- <GlobeIcon className="h-4 w-4" />
- <span>{currentLanguageText}</span>
- <ChevronDownIcon className="h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuRadioGroup
- value={i18n.language}
- onValueChange={(value) => handleChangeLanguage(value)}
- >
- <DropdownMenuRadioItem value="en">
- {t('languages.english')}
- </DropdownMenuRadioItem>
- <DropdownMenuRadioItem value="ko">
- {t('languages.korean')}
- </DropdownMenuRadioItem>
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- </div>
- </form>
- {/* ) : (
- <form onSubmit={handleOtpSubmit} className="flex flex-col gap-4 p-6 md:p-8">
- <div className="flex flex-col gap-6">
- <div className="flex flex-col items-center text-center">
- <h1 className="text-2xl font-bold">{t('loginMessage')}</h1>
- </div>
- <div className="grid gap-2 justify-center">
- <InputOTP
- maxLength={6}
- value={otp}
- onChange={(value) => setOtp(value)}
- >
- <InputOTPGroup>
- <InputOTPSlot index={0} />
- <InputOTPSlot index={1} />
- <InputOTPSlot index={2} />
- <InputOTPSlot index={3} />
- <InputOTPSlot index={4} />
- <InputOTPSlot index={5} />
- </InputOTPGroup>
- </InputOTP>
- </div>
- <Button type="submit" className="w-full" disabled={isLoading}>
- {isLoading ? t('verifying') : t('verifyOtp')}
- </Button>
- <div className="mx-auto">
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" className="flex items-center gap-2">
- <GlobeIcon className="h-4 w-4" />
- <span>{currentLanguageText}</span>
- <ChevronDownIcon className="h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuRadioGroup
- value={i18n.language}
- onValueChange={(value) => handleChangeLanguage(value)}
- >
- <DropdownMenuRadioItem value="en">
- {t('languages.english')}
- </DropdownMenuRadioItem>
- <DropdownMenuRadioItem value="ko">
- {t('languages.korean')}
- </DropdownMenuRadioItem>
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- </div>
- </form>
- )} */}
-
- <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
- {t('termsMessage')} <a href="#">{t('termsOfService')}</a> {t('and')}
- <a href="#">{t('privacyPolicy')}</a>.
- </div>
- </div>
- </div>
- </div>
-
- {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */}
- <div className="relative hidden h-full flex-col p-10 text-white dark:border-r md:flex">
- {/* Image 컴포넌트로 대체 */}
- <div className="absolute inset-0">
- <Image
- src="/images/02.jpg"
- alt="Background image"
- fill
- priority
- sizes="(max-width: 1024px) 100vw, 50vw"
- className="object-cover"
- />
- </div>
- <div className="relative z-10 mt-auto">
- <blockquote className="space-y-2">
- <p className="text-sm">&ldquo;{t("blockquote")}&rdquo;</p>
- {/* <footer className="text-sm">SHI</footer> */}
- </blockquote>
- </div>
- </div>
- </div>
- )
-} \ 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',