All files / src proxy.ts

0% Statements 0/95
0% Branches 0/1
0% Functions 0/1
0% Lines 0/95

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96                                                                                                                                                                                               
import { NextRequest, NextResponse } from 'next/server';

import { RefreshResponse } from './types/service/auth';

const GUEST_ONLY_PATHS = ['/login', '/signup'];
const MEMBER_ONLY_PATHS = ['/mypage', '/create-group', '/message', '/schedule', '/notification'];

export const proxy = async (request: NextRequest) => {
  const accessToken = request.cookies.get('accessToken');
  const refreshToken = request.cookies.get('refreshToken');
  let isLoggedIn = !!accessToken;
  let refreshFailed = false;

  const isGuestOnly = GUEST_ONLY_PATHS.some((path) => request.nextUrl.pathname.startsWith(path));
  const isMemberOnly = MEMBER_ONLY_PATHS.some((path) => request.nextUrl.pathname.startsWith(path));

  // 일반 응답 생성
  const response = NextResponse.next();

  // accessToken이 없을 때 refreshToken 있으면 refresh 시도 - 응답에 set cookie 설정
  if (!isLoggedIn && refreshToken) {
    try {
      const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/refresh`, {
        method: 'POST',
        headers: { Cookie: `refreshToken=${refreshToken.value}` },
      });
      if (!res.ok) throw new Error('refresh failed');
      const json = await res.json();
      const data: RefreshResponse = json.data;
      isLoggedIn = true;
      response.cookies.set('accessToken', data.accessToken, {
        httpOnly: false,
        maxAge: data.expiresIn,
        domain: 'wego.monster',
        secure: process.env.NODE_ENV === 'production',
      });
      // 서버가 발급한 새 refreshToken Set-Cookie 헤더를 브라우저에 포워딩
      const setCookieHeader = res.headers.get('set-cookie');
      if (setCookieHeader) {
        response.headers.append('Set-Cookie', setCookieHeader);
      }
    } catch (err) {
      console.log('refresh failed', err);
      isLoggedIn = false;
      refreshFailed = true;
      response.cookies.set('refreshToken', '', {
        maxAge: 0,
        domain: 'wego.monster',
        path: '/',
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
      });
    }
  }

  // 로그인 상태에서 Guest Only Path 에 접근 시 / 로 Redirect
  if (isGuestOnly && isLoggedIn) {
    return NextResponse.redirect(new URL('/', request.url));
  }

  // 로그아웃 상태에서 Member Only Path에 접근 시 /login 으로 Redirect
  if (isMemberOnly && !isLoggedIn) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('error', 'unauthorized');
    loginUrl.searchParams.set('path', request.nextUrl.pathname);
    const redirectResponse = NextResponse.redirect(loginUrl);
    if (refreshFailed) {
      redirectResponse.cookies.set('refreshToken', '', {
        maxAge: 0,
        domain: 'wego.monster',
        path: '/',
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
      });
    }
    return redirectResponse;
  }

  return response;
};

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

// 0. refreshToken만 있고 accessToken이 없는 경우 refresh 시도
// 0-1. refreshToken이 유효하지 않을 때는 로그아웃 상태로 판정됨. 이후 규칙에 의해 동작이 결정됨
// 1. 로그인 상태에서 /login, /signup 접근 시 /로 redirect
// 2. 로그아웃 상태에서 인증이 필요한 경로 접근 시 /login으로 redirect
// 3. member only 도 아니고 guest only 도 아닌 경로 접근 시 그대로 통과(ex. / 접근 시)

// 기본 정보
// - logout API는 accessToken이 유효하지 않을 경우 401 에러 반환됨.
// - 즉 refreshToken이 저장되어있지만 유효하지 않으면 logout api 실행 불가
// - 따라서 refreshToken이 유효하지 않을 경우 logout api를 호출하는 것이 아닌 직접 setcookie 설정으로 cookie 정보를 삭제해야함.