diff --git a/src/api/core/auth/index.ts b/src/api/core/refresh/index.ts similarity index 64% rename from src/api/core/auth/index.ts rename to src/api/core/refresh/index.ts index b57883fe..f77b035f 100644 --- a/src/api/core/auth/index.ts +++ b/src/api/core/refresh/index.ts @@ -1,17 +1,16 @@ import axios from 'axios'; -import { getAccessToken } from '@/lib/auth/token'; import { CommonErrorResponse } from '@/types/service/common'; import { createApiHelper } from '../lib/apiHelper'; -const authInstance = axios.create({ +const refreshInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, timeout: 20000, withCredentials: true, }); -authInstance.interceptors.request.use(async (config) => { +refreshInstance.interceptors.request.use(async (config) => { const isServer = typeof window === 'undefined'; if (isServer) { @@ -23,15 +22,10 @@ authInstance.interceptors.request.use(async (config) => { } } - const accessToken = await getAccessToken(); - if (accessToken && config.headers) { - config.headers.Authorization = `Bearer ${accessToken}`; - } - return config; }); -authInstance.interceptors.response.use( +refreshInstance.interceptors.response.use( (response) => { return response; }, @@ -40,4 +34,4 @@ authInstance.interceptors.response.use( }, ); -export const authAPI = createApiHelper(authInstance); +export const refreshAPI = createApiHelper(refreshInstance); diff --git a/src/api/service/auth-service/index.ts b/src/api/service/auth-service/index.ts index ebeb7171..4c00f6d2 100644 --- a/src/api/service/auth-service/index.ts +++ b/src/api/service/auth-service/index.ts @@ -1,4 +1,5 @@ -import { authAPI } from '@/api/core/auth'; +import { baseAPI } from '@/api/core/base'; +import { refreshAPI } from '@/api/core/refresh'; import { clearAccessToken, setAccessToken } from '@/lib/auth/token'; import { GoogleOAuthExchangeRequest, @@ -13,7 +14,8 @@ import { export const authServiceRemote = () => ({ // 로그인 login: async (payload: LoginRequest) => { - const data = await authAPI.post('/api/v1/auth/login', payload); + //prettier-ignore + const data = await baseAPI.post('/api/v1/auth/login', payload, { withCredentials: true }); setAccessToken(data.accessToken, data.expiresIn); return data; @@ -21,32 +23,36 @@ export const authServiceRemote = () => ({ // 회원가입 signup: async (payload: SignupRequest) => { - return authAPI.post(`/api/v1/auth/signup`, payload); + return baseAPI.post(`/api/v1/auth/signup`, payload, { withCredentials: true }); }, // 로그아웃 logout: async () => { - await authAPI.post('/api/v1/auth/logout', null); + await baseAPI.post('/api/v1/auth/logout', null, { withCredentials: true }); + clearAccessToken(); }, // 액세스 토큰 재발급 refresh: async () => { //prettier-ignore - const data = await authAPI.post('/api/v1/auth/refresh', {}); + const data = await refreshAPI.post('/api/v1/auth/refresh', null, { withCredentials: true }); + setAccessToken(data.accessToken, data.expiresIn); return data; }, // 회원 탈퇴 withdraw: async () => { - await authAPI.delete('/api/v1/auth/withdraw'); + await baseAPI.delete('/api/v1/auth/withdraw', { withCredentials: true }); + clearAccessToken(); }, // 구글 OAuth 코드 교환 exchangeGoogleCode: async (payload: GoogleOAuthExchangeRequest) => { - const data = await authAPI.post('/api/v1/auth/google', payload); + //prettier-ignore + const data = await baseAPI.post('/api/v1/auth/google', payload, { withCredentials: true }); setAccessToken(data.accessToken, data.expiresIn); return data; diff --git a/src/mock/service/auth/auth-utils.ts b/src/mock/service/auth/auth-utils.ts index 15b03e2a..b61c2fe0 100644 --- a/src/mock/service/auth/auth-utils.ts +++ b/src/mock/service/auth/auth-utils.ts @@ -13,6 +13,7 @@ const createMockTokens = () => ({ accessToken: 'mock-access-token', tokenType: 'Bearer' as const, expiresIn: 3600, + expiresAt: '2026-02-21T11:05:19.595700269', }); export const createLoginResponse = (email: string, password: string): LoginResponse => { diff --git a/src/providers/provider-auth/index.tsx b/src/providers/provider-auth/index.tsx index 011eb3a0..902ae969 100644 --- a/src/providers/provider-auth/index.tsx +++ b/src/providers/provider-auth/index.tsx @@ -1,8 +1,4 @@ -import React, { createContext, SetStateAction, useContext, useEffect, useState } from 'react'; - -import Cookies from 'js-cookie'; - -import { API } from '@/api'; +import React, { createContext, SetStateAction, useContext, useState } from 'react'; interface AuthContextType { isAuthenticated: boolean; @@ -25,25 +21,6 @@ interface Props { export const AuthProvider = ({ children, hasRefreshToken }: Props) => { const [isAuthenticated, setIsAuthenticated] = useState(hasRefreshToken); - // 초기값 설정 - // 페이지가 새로고침 될 때 accessToken이 없으면 refresh 시도, state update 실행 - useEffect(() => { - const updateAuthenticated = async () => { - const hasAccessToken = !!Cookies.get('accessToken'); - if (!hasAccessToken && hasRefreshToken) { - try { - await API.authService.refresh(); - setIsAuthenticated(true); - } catch { - setIsAuthenticated(false); - } - } else if (hasAccessToken) { - setIsAuthenticated(true); - } - }; - updateAuthenticated(); - }, [hasRefreshToken]); - return ( {children} diff --git a/src/proxy.ts b/src/proxy.ts index 4c6135e0..7cfc0986 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,8 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; -export const proxy = (request: NextRequest) => { +import { API } from './api'; + +export const proxy = async (request: NextRequest) => { const accessToken = request.cookies.get('accessToken'); const refreshToken = request.cookies.get('refreshToken'); + let hasValidToken = !!accessToken; const protectedPaths = ['/mypage', '/create-group', '/message', '/schedule', '/notification']; const isProtected = protectedPaths.some((path) => request.nextUrl.pathname.startsWith(path)); @@ -11,24 +14,45 @@ export const proxy = (request: NextRequest) => { const isPublic = publicPaths.some((path) => request.nextUrl.pathname.startsWith(path)); // 인증된 사용자가 public 페이지 접근 시 홈으로 - if (isPublic && (accessToken || refreshToken)) { + // refresh 중복 실행을 방지하기 위해 최상단으로 이동 + if (isPublic && refreshToken) { return NextResponse.redirect(new URL('/', request.url)); } + // 일반 응답 생성 + const response = NextResponse.next(); + + // accessToken이 없으면 refresh 실행하여 일반 응답에 set cookie 설정 + if (!accessToken && refreshToken) { + try { + const res = await API.authService.refresh(); + const data = res; + hasValidToken = true; + response.cookies.set('accessToken', data.accessToken, { + httpOnly: false, + maxAge: data.expiresIn, + domain: 'wego.monster', + secure: process.env.NODE_ENV === 'production', + }); + } catch { + hasValidToken = false; + } + } + // 보호되지 않은 경로는 그냥 통과 if (!isProtected) { - return NextResponse.next(); + return response; } - // 둘 다 없으면 로그인 페이지로 redirect - if (!accessToken && !refreshToken) { + // accessToken 없으면 login redirect + if (!hasValidToken) { const loginUrl = new URL('/login', request.url); loginUrl.searchParams.set('error', 'unauthorized'); loginUrl.searchParams.set('path', request.nextUrl.pathname); return NextResponse.redirect(loginUrl); } - return NextResponse.next(); + return response; }; export const config = { diff --git a/src/types/service/auth.ts b/src/types/service/auth.ts index 459d2246..b899e744 100644 --- a/src/types/service/auth.ts +++ b/src/types/service/auth.ts @@ -20,6 +20,7 @@ export interface LoginResponse { accessToken: string; tokenType: 'Bearer'; expiresIn: number; + expiresAt: string; user: { userId: number; email: string;