From e9897d416b3e7327bbd4d4aef887eee37751ae82 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 27 Jun 2025 01:16:20 +0000 Subject: (대표님) 20250627 오전 10시 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/use-next-auth-reauth.ts | 76 +++++++++++++++++++++++++++ hooks/use-settings-access.ts | 118 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 hooks/use-next-auth-reauth.ts create mode 100644 hooks/use-settings-access.ts (limited to 'hooks') diff --git a/hooks/use-next-auth-reauth.ts b/hooks/use-next-auth-reauth.ts new file mode 100644 index 00000000..6c17ddae --- /dev/null +++ b/hooks/use-next-auth-reauth.ts @@ -0,0 +1,76 @@ +// hooks/use-next-auth-reauth.ts +"use client" + +import * as React from "react" +import { useSession } from "next-auth/react" + +interface UseNextAuthReAuthOptions { + // 재인증 유효 시간 (밀리초, 기본값: 5분) + validDuration?: number + // 재인증이 필요한지 여부 + requireReAuth?: boolean +} + +export function useNextAuthReAuth(options: UseNextAuthReAuthOptions = {}) { + const { validDuration = 5 * 60 * 1000, requireReAuth = true } = options + const { data: session, status, update } = useSession() + + const [showReAuthModal, setShowReAuthModal] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(true) + + // 재인증이 필요한지 확인 + const isAuthenticated = React.useMemo(() => { + if (!session || !requireReAuth) { + return status === "authenticated" + } + + // JWT 토큰에서 재인증 시간 확인 + const reAuthTime = session.user?.reAuthTime + if (!reAuthTime) return false + + const now = Date.now() + return (now - reAuthTime) < validDuration + }, [session, requireReAuth, validDuration, status]) + + React.useEffect(() => { + if (status === "loading") return + + if (status === "unauthenticated") { + setIsLoading(false) + return + } + + if (requireReAuth && !isAuthenticated) { + setShowReAuthModal(true) + } + + setIsLoading(false) + }, [status, requireReAuth, isAuthenticated]) + + const handleReAuthSuccess = React.useCallback(async () => { + // 세션 업데이트 (재인증 시간 포함) + await update({ + reAuthTime: Date.now() + }) + setShowReAuthModal(false) + }, [update]) + + const forceReAuth = React.useCallback(async () => { + // 재인증 강제 실행 + await update({ + reAuthTime: null + }) + setShowReAuthModal(true) + }, [update]) + + return { + isAuthenticated, + showReAuthModal, + isLoading, + userEmail: session?.user?.email || "", + handleReAuthSuccess, + forceReAuth, + sessionStatus: status, + session, + } +} \ No newline at end of file diff --git a/hooks/use-settings-access.ts b/hooks/use-settings-access.ts new file mode 100644 index 00000000..cd7938ac --- /dev/null +++ b/hooks/use-settings-access.ts @@ -0,0 +1,118 @@ +// hooks/use-settings-access.ts (개선된 버전) +"use client" + +import * as React from "react" +import { useSession } from "next-auth/react" +import { useRouter } from "next/navigation" + +// 인증 방식 타입 +type AuthMethod = 'otp' | 'email' | 'sgips' | 'saml' + +interface UseSettingsAccessOptions { + // 재인증 유효 시간 (밀리초, 기본값: 5분) + validDuration?: number + // S-Gips 사용자 리다이렉트 경로 + sgipsRedirectPath?: string +} + +type AccessType = 'allowed' | 'reauth_required' | 'blocked_sgips' | 'loading' | 'unauthenticated' + +interface SettingsAccessState { + accessType: AccessType + showReAuthModal: boolean + isAuthenticated: boolean + userEmail: string + userId: string + userDomain: string | null + authMethod: AuthMethod | undefined + handleReAuthSuccess: () => Promise + forceReAuth: () => void +} + +export function useSettingsAccess(options: UseSettingsAccessOptions = {}): SettingsAccessState { + const { + validDuration = 5 * 60 * 1000, + sgipsRedirectPath = "/dashboard" + } = options + + const { data: session, status, update } = useSession() + const router = useRouter() + const [showReAuthModal, setShowReAuthModal] = React.useState(false) + + // 사용자의 접근 타입 결정 (인증 방식 기반) + const accessType: AccessType = React.useMemo(() => { + if (status === "loading") return 'loading' + if (status === "unauthenticated") return 'unauthenticated' + if (!session?.user) return 'unauthenticated' + + const authMethod = session.user.authMethod + + // 인증 방식에 따른 접근 제어 + switch (authMethod) { + case 'sgips': + // S-Gips 사용자는 접근 차단 + return 'blocked_sgips' + + case 'saml': + // SAML 사용자는 바로 접근 허용 + return 'allowed' + + case 'otp': + // OTP 사용자는 바로 접근 허용 (이미 2차 인증 완료) + return 'allowed' + + case 'email': + // 일반 이메일 사용자는 재인증 필요 + const reAuthTime = session.user.reAuthTime + if (!reAuthTime) return 'reauth_required' + + const now = Date.now() + const isReAuthValid = (now - reAuthTime) < validDuration + + return isReAuthValid ? 'allowed' : 'reauth_required' + + default: + // 인증 방식이 불명확한 경우 재인증 요구 + console.warn('Unknown auth method:', authMethod) + return 'reauth_required' + } + }, [session, status, validDuration]) + + // S-Gips 사용자 자동 리다이렉트 + React.useEffect(() => { + if (accessType === 'blocked_sgips') { + router.push(sgipsRedirectPath) + } + }, [accessType, router, sgipsRedirectPath]) + + // 재인증 필요 시 모달 표시 + React.useEffect(() => { + setShowReAuthModal(accessType === 'reauth_required') + }, [accessType]) + + const handleReAuthSuccess = React.useCallback(async () => { + await update({ + reAuthTime: Date.now() + }) + setShowReAuthModal(false) + }, [update]) + + const forceReAuth = React.useCallback(async () => { + await update({ + reAuthTime: null + }) + setShowReAuthModal(true) + }, [update]) + + return { + accessType, + showReAuthModal, + isAuthenticated: accessType === 'allowed', + userEmail: session?.user?.email || "", + userId: session?.user?.id || "", + userDomain: session?.user?.domain || null, + authMethod: session?.user?.authMethod, + handleReAuthSuccess, + forceReAuth, + } +} \ No newline at end of file -- cgit v1.2.3