diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
| commit | e9897d416b3e7327bbd4d4aef887eee37751ae82 (patch) | |
| tree | bd20ce6eadf9b21755bd7425492d2d31c7700a0e /middleware.ts | |
| parent | 3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff) | |
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'middleware.ts')
| -rw-r--r-- | middleware.ts | 151 |
1 files changed, 111 insertions, 40 deletions
diff --git a/middleware.ts b/middleware.ts index ff483818..ed471109 100644 --- a/middleware.ts +++ b/middleware.ts @@ -17,7 +17,8 @@ const publicPaths = [ '/partners/repository', '/partners/signup', '/api/auth', - '/spreadTest' + '/spreadTest', + '/auth/reset-password', ]; // 경로가 공개 경로인지 확인하는 함수 @@ -61,6 +62,50 @@ function getDomainRedirectPath(path: string, domain: string, lng: string) { return null; } +// 세션 타임아웃 체크 함수 +function checkSessionTimeout(token: any): { isExpired: boolean; isExpiringSoon: boolean } { + if (!token?.sessionExpiredAt) { + return { isExpired: false, isExpiringSoon: false }; + } + + const now = Date.now(); + const expiresAt = token.sessionExpiredAt; + const timeUntilExpiry = expiresAt - now; + const warningThreshold = 10 * 60 * 1000; // 10분 + + return { + isExpired: timeUntilExpiry <= 0, + isExpiringSoon: timeUntilExpiry <= warningThreshold && timeUntilExpiry > 0 + }; +} + +// 로그인 페이지 URL 생성 함수 (세션 만료 정보 포함) +function createLoginUrl(pathname: string, detectedLng: string, origin: string, request: NextRequest, reason?: string) { + let loginPath; + + // 경로에 따라 적절한 로그인 페이지 선택 + if (pathname.includes('/partners') || pathname.startsWith(`/${detectedLng}/vendor`)) { + loginPath = `/${detectedLng}/partners`; + } else { + loginPath = `/${detectedLng}/evcp`; + } + + const redirectUrl = new URL(loginPath, origin); + + // 로그인 후 원래 페이지로 리다이렉트하기 위해 callbackUrl 추가 + redirectUrl.searchParams.set('callbackUrl', request.nextUrl.pathname + request.nextUrl.search); + + // 세션 만료 관련 정보 추가 + if (reason) { + redirectUrl.searchParams.set('reason', reason); + if (reason === 'expired') { + redirectUrl.searchParams.set('message', '세션이 만료되었습니다. 다시 로그인해주세요.'); + } + } + + return redirectUrl; +} + export async function middleware(request: NextRequest) { /** * 1. 쿠키에서 언어 가져오기 @@ -110,7 +155,22 @@ export async function middleware(request: NextRequest) { const token = await getToken({ req: request }); /** - * 6. 인증된 사용자의 도메인-URL 일치 확인 및 리다이렉션 + * 6. 세션 타임아웃 체크 (인증된 사용자에 대해서만) + */ + if (token && !isPublicPath(pathname, detectedLng)) { + const { isExpired, isExpiringSoon } = checkSessionTimeout(token); + + if (isExpired) { + console.log(`Session expired in middleware for user ${token.email}`); + const loginUrl = createLoginUrl(pathname, detectedLng, origin, request, 'expired'); + return NextResponse.redirect(loginUrl); + } + + // 세션 만료 경고를 위한 응답 헤더 설정은 나중에 적용 + } + + /** + * 7. 인증된 사용자의 도메인-URL 일치 확인 및 리다이렉션 */ if (token && token.domain && !isPublicPath(pathname, detectedLng)) { // 사용자의 domain과 URL 경로가 일치하는지 확인 @@ -125,57 +185,68 @@ export async function middleware(request: NextRequest) { } /** - * 6.5 이미 로그인한 사용자가 로그인 페이지에 접근할 경우 대시보드로 리다이렉트 - */ -if (token) { - // 로그인 페이지 경로 확인 (정확한 /ko/evcp 또는 /en/partners 등) - const isEvcpLoginPage = pathname === `/${detectedLng}/evcp`; - const isPartnersLoginPage = pathname === `/${detectedLng}/partners`; - - if (isEvcpLoginPage) { - // EVCP 로그인 페이지에 접근한 경우 report 페이지로 리다이렉트 - const redirectUrl = new URL(`/${detectedLng}/evcp/report`, origin); - redirectUrl.search = searchParams.toString(); - return NextResponse.redirect(redirectUrl); - } else if (isPartnersLoginPage) { - // Partners 로그인 페이지에 접근한 경우 dashboard 페이지로 리다이렉트 - const redirectUrl = new URL(`/${detectedLng}/partners/dashboard`, origin); - redirectUrl.search = searchParams.toString(); - return NextResponse.redirect(redirectUrl); + * 8. 이미 로그인한 사용자가 로그인 페이지에 접근할 경우 대시보드로 리다이렉트 + */ + if (token) { + // 세션이 만료되지 않은 경우에만 대시보드로 리다이렉트 + const { isExpired } = checkSessionTimeout(token); + + if (!isExpired) { + // 로그인 페이지 경로 확인 (정확한 /ko/evcp 또는 /en/partners 등) + const isEvcpLoginPage = pathname === `/${detectedLng}/evcp`; + const isPartnersLoginPage = pathname === `/${detectedLng}/partners`; + + if (isEvcpLoginPage) { + // EVCP 로그인 페이지에 접근한 경우 report 페이지로 리다이렉트 + const redirectUrl = new URL(`/${detectedLng}/evcp/report`, origin); + redirectUrl.search = searchParams.toString(); + return NextResponse.redirect(redirectUrl); + } else if (isPartnersLoginPage) { + // Partners 로그인 페이지에 접근한 경우 dashboard 페이지로 리다이렉트 + const redirectUrl = new URL(`/${detectedLng}/partners/dashboard`, origin); + redirectUrl.search = searchParams.toString(); + return NextResponse.redirect(redirectUrl); + } + } } -} - /** - * 7. 인증 확인: 공개 경로가 아닌 경우 로그인 체크 및 리다이렉트 + * 9. 인증 확인: 공개 경로가 아닌 경우 로그인 체크 및 리다이렉트 */ if (!isPublicPath(pathname, detectedLng)) { if (!token) { - // 어떤 로그인 페이지로 리다이렉트할지 결정 - let loginPath; - - // 경로에 따라 적절한 로그인 페이지 선택 - if (pathname.includes('/partners') || pathname.startsWith(`/${detectedLng}/vendor`)) { - loginPath = `/${detectedLng}/partners`; - } else { - loginPath = `/${detectedLng}/evcp`; - } - - // 로그인 후 원래 페이지로 리다이렉트하기 위해 callbackUrl 추가 - const redirectUrl = new URL(loginPath, origin); - redirectUrl.searchParams.set('callbackUrl', request.nextUrl.pathname + request.nextUrl.search); - - return NextResponse.redirect(redirectUrl); + const loginUrl = createLoginUrl(pathname, detectedLng, origin, request); + return NextResponse.redirect(loginUrl); + } + + // 토큰은 있지만 세션이 만료된 경우 (이미 위에서 처리되었지만 추가 안전장치) + const { isExpired } = checkSessionTimeout(token); + if (isExpired) { + const loginUrl = createLoginUrl(pathname, detectedLng, origin, request, 'expired'); + return NextResponse.redirect(loginUrl); } } /** - * 8. 위 조건에 걸리지 않았다면 그대로 Next.js로 넘긴다. + * 10. 위 조건에 걸리지 않았다면 그대로 Next.js로 넘긴다. */ const response = NextResponse.next(); /** - * 9. 쿠키에 저장된 언어와 현재 lng가 다르면 업데이트 + * 11. 세션 만료 경고를 위한 헤더 추가 + */ + if (token && !isPublicPath(pathname, detectedLng)) { + const { isExpiringSoon } = checkSessionTimeout(token); + + if (isExpiringSoon && token.sessionExpiredAt) { + response.headers.set('X-Session-Warning', 'true'); + response.headers.set('X-Session-Expires-At', token.sessionExpiredAt.toString()); + response.headers.set('X-Session-Time-Left', (token.sessionExpiredAt - Date.now()).toString()); + } + } + + /** + * 12. 쿠키에 저장된 언어와 현재 lng가 다르면 업데이트 */ const currentCookie = request.cookies.get(cookieName)?.value; if (detectedLng && detectedLng !== currentCookie) { @@ -186,7 +257,7 @@ if (token) { } /** - * 10. 매칭할 경로 설정 + * 13. 매칭할 경로 설정 */ export const config = { matcher: [ |
