11import { NextRequest , NextResponse } from 'next/server' ;
22
3- import { API } from './api' ;
3+ import { RefreshResponse } from './types/service/auth' ;
4+
5+ const GUEST_ONLY_PATHS = [ '/login' , '/signup' ] ;
6+ const MEMBER_ONLY_PATHS = [ '/mypage' , '/create-group' , '/message' , '/schedule' , '/notification' ] ;
47
58export const proxy = async ( request : NextRequest ) => {
69 const accessToken = request . cookies . get ( 'accessToken' ) ;
710 const refreshToken = request . cookies . get ( 'refreshToken' ) ;
8- let hasValidToken = ! ! accessToken ;
9-
10- const protectedPaths = [ '/mypage' , '/create-group' , '/message' , '/schedule' , '/notification' ] ;
11- const isProtected = protectedPaths . some ( ( path ) => request . nextUrl . pathname . startsWith ( path ) ) ;
11+ let isLoggedIn = ! ! accessToken ;
12+ let refreshFailed = false ;
1213
13- const publicPaths = [ '/login' , '/signup' ] ;
14- const isPublic = publicPaths . some ( ( path ) => request . nextUrl . pathname . startsWith ( path ) ) ;
15-
16- // 인증된 사용자가 public 페이지 접근 시 홈으로
17- // refresh 중복 실행을 방지하기 위해 최상단으로 이동
18- if ( isPublic && refreshToken ) {
19- return NextResponse . redirect ( new URL ( '/' , request . url ) ) ;
20- }
14+ const isGuestOnly = GUEST_ONLY_PATHS . some ( ( path ) => request . nextUrl . pathname . startsWith ( path ) ) ;
15+ const isMemberOnly = MEMBER_ONLY_PATHS . some ( ( path ) => request . nextUrl . pathname . startsWith ( path ) ) ;
2116
2217 // 일반 응답 생성
2318 const response = NextResponse . next ( ) ;
2419
25- // accessToken이 없으면 refresh 실행하여 일반 응답에 set cookie 설정
26- if ( ! accessToken && refreshToken ) {
20+ // accessToken이 없을 때 refreshToken 있으면 refresh 시도 - 응답에 set cookie 설정
21+ if ( ! isLoggedIn && refreshToken ) {
2722 try {
28- const res = await API . authService . refresh ( ) ;
29- const data = res ;
30- hasValidToken = true ;
23+ const res = await fetch ( `${ process . env . NEXT_PUBLIC_API_BASE_URL } /api/v1/auth/refresh` , {
24+ method : 'POST' ,
25+ headers : { Cookie : `refreshToken=${ refreshToken . value } ` } ,
26+ } ) ;
27+ if ( ! res . ok ) throw new Error ( 'refresh failed' ) ;
28+ const json = await res . json ( ) ;
29+ const data : RefreshResponse = json . data ;
30+ isLoggedIn = true ;
3131 response . cookies . set ( 'accessToken' , data . accessToken , {
3232 httpOnly : false ,
3333 maxAge : data . expiresIn ,
3434 domain : 'wego.monster' ,
3535 secure : process . env . NODE_ENV === 'production' ,
3636 } ) ;
37- } catch {
38- hasValidToken = false ;
37+ // 서버가 발급한 새 refreshToken Set-Cookie 헤더를 브라우저에 포워딩
38+ const setCookieHeader = res . headers . get ( 'set-cookie' ) ;
39+ if ( setCookieHeader ) {
40+ response . headers . append ( 'Set-Cookie' , setCookieHeader ) ;
41+ }
42+ } catch ( err ) {
43+ console . log ( 'refresh failed' , err ) ;
44+ isLoggedIn = false ;
45+ refreshFailed = true ;
46+ response . cookies . set ( 'refreshToken' , '' , {
47+ maxAge : 0 ,
48+ domain : 'wego.monster' ,
49+ path : '/' ,
50+ httpOnly : true ,
51+ secure : process . env . NODE_ENV === 'production' ,
52+ } ) ;
3953 }
4054 }
4155
42- // 보호되지 않은 경로는 그냥 통과
43- if ( ! isProtected ) {
44- return response ;
56+ // 로그인 상태에서 Guest Only Path 에 접근 시 / 로 Redirect
57+ if ( isGuestOnly && isLoggedIn ) {
58+ return NextResponse . redirect ( new URL ( '/' , request . url ) ) ;
4559 }
4660
47- // accessToken 없으면 login redirect
48- if ( ! hasValidToken ) {
61+ // 로그아웃 상태에서 Member Only Path에 접근 시 / login 으로 Redirect
62+ if ( isMemberOnly && ! isLoggedIn ) {
4963 const loginUrl = new URL ( '/login' , request . url ) ;
5064 loginUrl . searchParams . set ( 'error' , 'unauthorized' ) ;
5165 loginUrl . searchParams . set ( 'path' , request . nextUrl . pathname ) ;
52- return NextResponse . redirect ( loginUrl ) ;
66+ const redirectResponse = NextResponse . redirect ( loginUrl ) ;
67+ if ( refreshFailed ) {
68+ redirectResponse . cookies . set ( 'refreshToken' , '' , {
69+ maxAge : 0 ,
70+ domain : 'wego.monster' ,
71+ path : '/' ,
72+ httpOnly : true ,
73+ secure : process . env . NODE_ENV === 'production' ,
74+ } ) ;
75+ }
76+ return redirectResponse ;
5377 }
5478
5579 return response ;
@@ -58,3 +82,14 @@ export const proxy = async (request: NextRequest) => {
5882export const config = {
5983 matcher : [ '/((?!api|_next/static|_next/image|favicon.ico).*)' ] ,
6084} ;
85+
86+ // 0. refreshToken만 있고 accessToken이 없는 경우 refresh 시도
87+ // 0-1. refreshToken이 유효하지 않을 때는 로그아웃 상태로 판정됨. 이후 규칙에 의해 동작이 결정됨
88+ // 1. 로그인 상태에서 /login, /signup 접근 시 /로 redirect
89+ // 2. 로그아웃 상태에서 인증이 필요한 경로 접근 시 /login으로 redirect
90+ // 3. member only 도 아니고 guest only 도 아닌 경로 접근 시 그대로 통과(ex. / 접근 시)
91+
92+ // 기본 정보
93+ // - logout API는 accessToken이 유효하지 않을 경우 401 에러 반환됨.
94+ // - 즉 refreshToken이 저장되어있지만 유효하지 않으면 logout api 실행 불가
95+ // - 따라서 refreshToken이 유효하지 않을 경우 logout api를 호출하는 것이 아닌 직접 setcookie 설정으로 cookie 정보를 삭제해야함.
0 commit comments