diff --git a/src/apis/queryFunctions/Auth.ts b/src/apis/queryFunctions/Auth.ts index c0a33168..99a9e7c9 100644 --- a/src/apis/queryFunctions/Auth.ts +++ b/src/apis/queryFunctions/Auth.ts @@ -1,6 +1,5 @@ import createApiClient from '@/apis/queryFunctions/apiClient'; import { - LoginParams, RegisterParams, CheckEmailParams, PostRegisterResponseData, @@ -14,10 +13,7 @@ const apiClient = createApiClient(); export const postRegister = (param: RegisterParams) => apiClient.post>('/v2/auth/register', param); -export const postLogin = (param: LoginParams) => - apiClient.post>('/v2/auth/login', param); - -export const postLogout = () => apiClient.post>('v2/auth/logout'); +export const postLogout = () => apiClient.post>('v2/auth/logout'); // 없어져야할 것 export const getEmailValidation = async (params: CheckEmailParams) => { const response = await apiClient.get>('v2/auth/validate-email', { diff --git a/src/apis/queryFunctions/apiClient.ts b/src/apis/queryFunctions/apiClient.ts index 66fb9b1a..865eba4b 100644 --- a/src/apis/queryFunctions/apiClient.ts +++ b/src/apis/queryFunctions/apiClient.ts @@ -1,37 +1,38 @@ import axios from 'axios'; -function createApiClient(cookie?: string) { +const isServer = typeof window === 'undefined'; + +function createApiClient() { const client = axios.create({ baseURL: `${process.env.NEXT_PUBLIC_SERVER_API_URL}/api`, timeout: 100000, withCredentials: true, headers: { - ...(cookie ? { Cookie: cookie } : {}), + 'Content-Type': 'application/json', }, }); - client.interceptors.request.use( - config => { - // 성공한 요청 + client.interceptors.request.use(async config => { + if (isServer) { + // 서버에서 실행되는 경우에는 쿠키에서 access_token 가져옵니다. + const { cookies } = await import('next/headers'); + const accessToken = cookies().get('accessToken')?.value; - return config; - }, - error => { - // 실패한 요청 - return Promise.reject(error); - }, - ); + if (accessToken && !config.headers.Authorization) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = accessToken; + } + } else { + // 클라이언트에서 실행되는 경우에는 스토리지에서 access_token 가져옵니다. + const accessToken = sessionStorage.getItem('accessToken'); - client.interceptors.response.use( - response => { - // 성공한 응답 - return response; - }, - error => { - // 실패한 응답 - return Promise.reject(error); - }, - ); + if (accessToken && !config.headers.Authorization) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = accessToken; + } + } + return config; + }); return client; } diff --git a/src/apis/queryFunctions/deleteTokenCookies.ts b/src/apis/queryFunctions/deleteTokenCookies.ts new file mode 100644 index 00000000..07f7e332 --- /dev/null +++ b/src/apis/queryFunctions/deleteTokenCookies.ts @@ -0,0 +1,11 @@ +'use server'; + +import { cookies } from 'next/headers'; + +function deleteTokenCookies() { + cookies().set('accessToken', '', { + maxAge: 0, + }); +} + +export default deleteTokenCookies; diff --git a/src/apis/queryFunctions/setTokenCookies.ts b/src/apis/queryFunctions/setTokenCookies.ts new file mode 100644 index 00000000..3cfa723b --- /dev/null +++ b/src/apis/queryFunctions/setTokenCookies.ts @@ -0,0 +1,26 @@ +'use server'; + +import { setCookies } from '@/utils/setCookies'; + +import { LoginParams } from '../types/Auth'; + +async function setTokenCookies(params: LoginParams) { + const loginData = await fetch(`${process.env.NEXT_PUBLIC_SERVER_API_URL}/api/v2/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }) + .then(res => res.json()) + .then(jsonRes => jsonRes.result_data); + + const accessToken = loginData.access_token; + const refreshToken = loginData.refresh_token; + + setCookies('accessToken', accessToken); + + return { accessToken, refreshToken }; +} + +export default setTokenCookies; diff --git a/src/apis/queryHooks/Auth/usePostLogin.ts b/src/apis/queryHooks/Auth/useLogin.ts similarity index 66% rename from src/apis/queryHooks/Auth/usePostLogin.ts rename to src/apis/queryHooks/Auth/useLogin.ts index e7fc3e8a..ffd58c39 100644 --- a/src/apis/queryHooks/Auth/usePostLogin.ts +++ b/src/apis/queryHooks/Auth/useLogin.ts @@ -1,31 +1,33 @@ import { useMutation } from '@tanstack/react-query'; import { AxiosError } from 'axios'; -import { useSearchParams, useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; -import { postLogin } from '@/apis/queryFunctions/Auth'; +import setTokenCookies from '@/apis/queryFunctions/setTokenCookies'; import { LoginParams } from '@/apis/types/Auth'; import { Response } from '@/apis/types/common'; import { useAuth } from '@/provider/authProvider'; import { useToast } from '@/provider/toastProvider'; -function usePostLogin() { +function useLogin() { + const { setIsLoggedIn } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); + const prevUrl = searchParams.get('prevUrl'); - const { setIsLoggedIn } = useAuth(); const { showToast } = useToast(); - const { mutate, error } = useMutation({ - mutationFn: (param: LoginParams) => postLogin(param), - onSuccess: () => { + const { mutate } = useMutation({ + mutationFn: (loginParams: LoginParams) => setTokenCookies(loginParams), + onSuccess: data => { + sessionStorage.setItem('accessToken', data.accessToken); + localStorage.setItem('refreshToken', data.refreshToken); + setIsLoggedIn(true); + showToast('success', '로그인 되었습니다.'); if (prevUrl?.startsWith('/join') || prevUrl?.startsWith('/login')) { router.push('/'); } else { router.push(prevUrl || '/'); } - router.refresh(); - setIsLoggedIn(true); - showToast('success', '로그인에 성공했습니다.'); }, onError: (e: AxiosError>) => { if (e.response) { @@ -37,7 +39,7 @@ function usePostLogin() { }, }); - return { mutate, error }; + return { mutate }; } -export default usePostLogin; +export default useLogin; diff --git a/src/apis/queryHooks/User/useGetUser.ts b/src/apis/queryHooks/User/useGetUser.ts index 4a3bccd9..95c0a999 100644 --- a/src/apis/queryHooks/User/useGetUser.ts +++ b/src/apis/queryHooks/User/useGetUser.ts @@ -5,6 +5,7 @@ import { useAuth } from '@/provider/authProvider'; function useGetUser() { const { isLoggedIn } = useAuth(); + const { data, isLoading } = useQuery({ queryKey: ['userInfo'], queryFn: () => getUser(), diff --git a/src/apis/types/Auth.ts b/src/apis/types/Auth.ts index fdb312bd..394d5e18 100644 --- a/src/apis/types/Auth.ts +++ b/src/apis/types/Auth.ts @@ -17,6 +17,11 @@ export interface LoginParams { password: string; } +export interface PostLoginResponseData { + access_token: string; + refresh_token: string; +} + export interface CheckEmailParams { email: string; } diff --git a/src/app/basicauction/[id]/HaveToLoginNotiModal.tsx b/src/app/basicauction/[id]/HaveToLoginNotiModal.tsx index e9712213..447805e5 100644 --- a/src/app/basicauction/[id]/HaveToLoginNotiModal.tsx +++ b/src/app/basicauction/[id]/HaveToLoginNotiModal.tsx @@ -6,6 +6,7 @@ import * as S from './HaveToLoginNotiModal.css'; function HaveToLoginNotiModal() { const { isLoggedIn } = useAuth(); + const router = useRouter(); const pathname = usePathname(); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 33f1a0e9..f643ed4e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -38,10 +38,11 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const cookie = cookies(); - const isLoggedIn = cookie.has('access'); + const isLoggedIn = !!cookies().get('accessToken')?.value; const queryClient = await usePrefetchQueriesWithCookie([ + // TODO 로그인이 필요한 호출을 따로 빼던가 불리해야함 + // 현재 useInfo의 경우 로그인을 해야만 호출 가능한데 프리패치로 미로그인 상태에서도 호출 되는 중 { queryKey: ['userInfo'], api: '/v2/member' }, { queryKey: ['category'], api: '/v2/categories' }, ]); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 516d42cf..98510e15 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -4,7 +4,7 @@ import { useForm, SubmitHandler } from 'react-hook-form'; import Link from 'next/link'; -import usePostLogin from '@/apis/queryHooks/Auth/usePostLogin'; +import useLogin from '@/apis/queryHooks/Auth/useLogin'; import GoogleIcon from '@/assets/svg/google.svg'; import NaverIcon from '@/assets/svg/naver.svg'; import CommonButton from '@/components/CommonButton'; @@ -26,8 +26,7 @@ function Home() { formState: { errors }, } = useForm(); - const { mutate: login } = usePostLogin(); - + const { mutate: login } = useLogin(); const onSubmit: SubmitHandler = async data => { const newPassword = await sha256(data.passwordRequired); diff --git a/src/components/AuctionInfo/AuctionBidListModal.tsx b/src/components/AuctionInfo/AuctionBidListModal.tsx index 729dd854..02ef6ea3 100644 --- a/src/components/AuctionInfo/AuctionBidListModal.tsx +++ b/src/components/AuctionInfo/AuctionBidListModal.tsx @@ -11,6 +11,7 @@ interface AuctionBidListModalProps { function AuctionBidListModal({ id }: AuctionBidListModalProps) { const { isLoggedIn } = useAuth(); + const router = useRouter(); const pathname = usePathname(); diff --git a/src/components/AuctionInfo/hooks/usePermissionBidPrice.ts b/src/components/AuctionInfo/hooks/usePermissionBidPrice.ts index 6dd87539..183f6e9d 100644 --- a/src/components/AuctionInfo/hooks/usePermissionBidPrice.ts +++ b/src/components/AuctionInfo/hooks/usePermissionBidPrice.ts @@ -6,6 +6,7 @@ import { useAuth } from '@/provider/authProvider'; export function usePermissionBidPrice(auctionId: number, sellerId: number) { const { isLoggedIn } = useAuth(); + const { data: user } = useGetUser(); const [expired, setExpired] = useState(''); const { data: auctionBidList } = useGetBasicAuctionBidList(auctionId); diff --git a/src/components/Chatting/ChattingIconButton.tsx b/src/components/Chatting/ChattingIconButton.tsx index 29c2ab5e..600e31bf 100644 --- a/src/components/Chatting/ChattingIconButton.tsx +++ b/src/components/Chatting/ChattingIconButton.tsx @@ -12,6 +12,7 @@ import * as S from './ChattingIconButton.css'; function ChattingIconButton() { const { isLoggedIn } = useAuth(); + const [isOpen, setIsOpen] = useState(false); if (!isLoggedIn) return null; diff --git a/src/components/Chatting/ChattingMessageSection.tsx b/src/components/Chatting/ChattingMessageSection.tsx index 352c3ef6..6c02bba2 100644 --- a/src/components/Chatting/ChattingMessageSection.tsx +++ b/src/components/Chatting/ChattingMessageSection.tsx @@ -20,6 +20,7 @@ interface ChattingMessageSectionProps { function ChattingMessageSection({ lastChat, roomId }: ChattingMessageSectionProps) { const { data: user } = useGetUser(); + const { refetch } = useGetChatroomList({ pageable: 0, }); diff --git a/src/components/Chatting/hooks/useChatSocket.ts b/src/components/Chatting/hooks/useChatSocket.ts index f96abacb..d38510ca 100644 --- a/src/components/Chatting/hooks/useChatSocket.ts +++ b/src/components/Chatting/hooks/useChatSocket.ts @@ -23,6 +23,8 @@ function useChatSocket({ onMessage, checkBottom, }: UseChatSocketParams) { + const accessToken = sessionStorage.getItem('accessToken'); + const [newMessage, setNewMessage] = useState(null); const user = useGetUser(); const timerRef = useRef(null); // 타이머 ID 저장 @@ -78,6 +80,7 @@ function useChatSocket({ }; const { client } = useSocket({ + access: accessToken, url: `${process.env.NEXT_PUBLIC_SOCKET_SERVER_URL}`, config: { // https://stomp-js.github.io/api-docs/latest/classes/Client.html @@ -86,6 +89,7 @@ function useChatSocket({ // 연결을 시도합니다. setMessages(lastChat); }, + onWebSocketError: error => { console.log('WebSocket Error :', error); }, diff --git a/src/components/HeaderSection/components/UserHeader/index.tsx b/src/components/HeaderSection/components/UserHeader/index.tsx index f8135560..904e8d6e 100644 --- a/src/components/HeaderSection/components/UserHeader/index.tsx +++ b/src/components/HeaderSection/components/UserHeader/index.tsx @@ -1,9 +1,13 @@ +'use client'; + import Image from 'next/image'; import Link from 'next/link'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import usePostLogout from '@/apis/queryHooks/Auth/usePostLogout'; +// import usePostLogout from '@/apis/queryHooks/Auth/usePostLogout'; +// import logout from '@/apis/queryFunctions/deleteCookies'; import logoIcon from '@/assets/png/logo.png'; +import useLogout from '@/hooks/useLogout'; import { useAuth } from '@/provider/authProvider'; import { SUB_CATEGORY } from '@/static/category'; @@ -11,11 +15,23 @@ import * as S from './UserHeader.css'; function UserHeader() { const router = useRouter(); + const { isLoggedIn } = useAuth(); - const { mutate: logout } = usePostLogout(); + + // const { mutate: logout } = usePostLogout(); const pathname = usePathname(); const searchParams = useSearchParams(); + const handleLogout = useLogout(); + + // const handleLogout = () => { + // logout(); + // localStorage.removeItem('refreshToken'); + // sessionStorage.removeItem('accessToken'); + + // router.push('/'); + // }; + return (
@@ -60,7 +76,7 @@ function UserHeader() { ); })} {isLoggedIn ? ( - ) : ( diff --git a/src/hooks/useLogout.ts b/src/hooks/useLogout.ts new file mode 100644 index 00000000..3dc48801 --- /dev/null +++ b/src/hooks/useLogout.ts @@ -0,0 +1,24 @@ +import { useRouter } from 'next/navigation'; + +import deleteCookies from '@/apis/queryFunctions/deleteTokenCookies'; +import { useAuth } from '@/provider/authProvider'; +import { useToast } from '@/provider/toastProvider'; + +function useLogout() { + const router = useRouter(); + const { setIsLoggedIn } = useAuth(); + const { showToast } = useToast(); + + const handleLogout = () => { + deleteCookies(); + localStorage.removeItem('refreshToken'); + sessionStorage.removeItem('accessToken'); + setIsLoggedIn(false); + router.push('/'); + showToast('success', '로그아웃 되었습니다.'); + }; + + return handleLogout; +} + +export default useLogout; diff --git a/src/hooks/usePrefetchQueriesWithCookie.ts b/src/hooks/usePrefetchQueriesWithCookie.ts index 9e0b5d40..44216c13 100644 --- a/src/hooks/usePrefetchQueriesWithCookie.ts +++ b/src/hooks/usePrefetchQueriesWithCookie.ts @@ -1,6 +1,5 @@ import { QueryClient, QueryKey } from '@tanstack/react-query'; import { AxiosError } from 'axios'; -import { cookies } from 'next/headers'; import createApiClient from '@/apis/queryFunctions/apiClient'; import { Response } from '@/apis/types/common'; @@ -14,8 +13,7 @@ interface UsePrefetchQueriesWithCookieProps { async function usePrefetchQueriesWithCookie( queries: UsePrefetchQueriesWithCookieProps[], ) { - const cookie = cookies(); - const apiClient = createApiClient(cookie.toString()); + const apiClient = createApiClient(); const queryClient = new QueryClient(); await Promise.all( diff --git a/src/hooks/usePrefetchQueryWithCookie.ts b/src/hooks/usePrefetchQueryWithCookie.ts index f797963b..8b452e82 100644 --- a/src/hooks/usePrefetchQueryWithCookie.ts +++ b/src/hooks/usePrefetchQueryWithCookie.ts @@ -1,5 +1,4 @@ import { QueryClient, QueryKey } from '@tanstack/react-query'; -import { cookies } from 'next/headers'; import createApiClient from '@/apis/queryFunctions/apiClient'; import { Response } from '@/apis/types/common'; @@ -14,8 +13,7 @@ async function usePrefetchQueryWithCookie({ queryKey, api, }: UsePrefetchQueryWithCookieProps) { - const cookie = cookies(); - const apiClient = createApiClient(cookie.toString()); + const apiClient = createApiClient(); const queryClient = new QueryClient(); await queryClient.prefetchQuery({ diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts index b8b5ce1d..e3f39f17 100644 --- a/src/hooks/useSocket.ts +++ b/src/hooks/useSocket.ts @@ -4,18 +4,22 @@ import * as StompJs from '@stomp/stompjs'; import SockJS from 'sockjs-client'; interface UseSocketParams { + access: string | null; url: string; config: StompJs.StompConfig; afterConnect: (client: StompJs.Client) => void; } -function useSocket({ url, config, afterConnect }: UseSocketParams) { +function useSocket({ access, url, config, afterConnect }: UseSocketParams) { const client = useRef(); const connect = () => { client.current = new StompJs.Client({ ...config, webSocketFactory: () => new SockJS(url), + connectHeaders: { + Authorization: access!, + }, }); client.current.onConnect = () => { @@ -33,7 +37,7 @@ function useSocket({ url, config, afterConnect }: UseSocketParams) { }; useEffect(() => { - connect(); + if (access) connect(); return () => { if (client) { diff --git a/src/middleware.ts b/src/middleware.ts index fce72e08..a95d2df6 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,3 +1,4 @@ +import { cookies } from 'next/headers'; import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; @@ -5,29 +6,23 @@ import type { NextRequest } from 'next/server'; const afterLoginProtectedRoutes = ['/login', '/join']; export function middleware(request: NextRequest) { - const refreshToken = request.cookies.get('refresh')?.value; - const accessToken = request.cookies.get('access')?.value; + const accessToken = cookies().get('accessToken')?.value; const { pathname } = request.nextUrl; - if (refreshToken && afterLoginProtectedRoutes.includes(pathname)) { + if (accessToken && afterLoginProtectedRoutes.includes(pathname)) { return NextResponse.redirect(new URL('/', request.url)); } - if (afterLoginProtectedRoutes.includes(pathname)) { + if (!accessToken && afterLoginProtectedRoutes.includes(pathname)) { return NextResponse.next(); } - if ((!refreshToken && !accessToken) || (!refreshToken && accessToken)) { + if (!accessToken) { const response = NextResponse.redirect( new URL('/login?alert=로그인 후 이용 가능한 서비스입니다.', request.url), ); - // refreshToken이 없고 accessToken이 있는 경우에만 동작하도록 조정 - if (!refreshToken && accessToken) { - response.cookies.delete('access'); // 여기서 accessToken 삭제 - } - return response; } @@ -38,3 +33,5 @@ export function middleware(request: NextRequest) { export const config = { matcher: ['/mypage/:path*', '/create/:path*', '/login', '/join'], }; + +// 미들웨어에서 토큰이 isExpired인지 확인하는 것을 넣으면 좋겠구먼.. 아니면 interceptor에서든지 diff --git a/src/utils/setCookies.ts b/src/utils/setCookies.ts new file mode 100644 index 00000000..2a3813b5 --- /dev/null +++ b/src/utils/setCookies.ts @@ -0,0 +1,10 @@ +import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'; +import { cookies } from 'next/headers'; + +export const setCookies = (key: string, value: string, options?: Partial) => { + return cookies().set(key, value, { + ...options, + maxAge: 1800, // 30분 + httpOnly: true, + }); +};