diff --git a/src/app/(board)/board/create/components/BoardNameForm.tsx b/src/app/(board)/board/create/components/BoardNameForm.tsx index 7eaa32e..b57d613 100644 --- a/src/app/(board)/board/create/components/BoardNameForm.tsx +++ b/src/app/(board)/board/create/components/BoardNameForm.tsx @@ -1,10 +1,10 @@ 'use client' -import TextInput from '@/components/TextInput' -import { ReactNode, useState } from 'react' import Button from '@/components/Button' +import TextInput from '@/components/TextInput' import { postBoard } from '@/lib' import { useRouter } from 'next/navigation' +import { ReactNode, useState } from 'react' const MAX_BOARD_NAME_LENGTH = 15 diff --git a/src/app/(onboarding)/signup/components/NicknameForm.tsx b/src/app/(onboarding)/signup/components/NicknameForm.tsx index 69ea9b3..43ea7e8 100644 --- a/src/app/(onboarding)/signup/components/NicknameForm.tsx +++ b/src/app/(onboarding)/signup/components/NicknameForm.tsx @@ -2,6 +2,7 @@ import Button from '@/components/Button' import NicknameInput from '@/components/TextInput/NicknameInput' +import { changeNickname } from '@/lib' import { useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' import SketchIcon from 'public/icons/sketchIcons-1.svg' @@ -18,6 +19,7 @@ const NicknameForm = () => { update({ name: nickname, }) + await changeNickname(nickname) router.push('/signup/complete') } diff --git a/src/app/mypage/profileEdit/components/NicknameForm.tsx b/src/app/mypage/profileEdit/components/NicknameForm.tsx index a8ab987..f1db913 100644 --- a/src/app/mypage/profileEdit/components/NicknameForm.tsx +++ b/src/app/mypage/profileEdit/components/NicknameForm.tsx @@ -4,6 +4,7 @@ import Button from '@/components/Button' import { useSession } from 'next-auth/react' import { ReactNode, useState } from 'react' import NicknameInput from '@/components/TextInput/NicknameInput' +import { changeNickname } from '@/lib' import Title from './Title' const NicknameForm = ({ children }: { children: ReactNode }) => { @@ -15,6 +16,7 @@ const NicknameForm = ({ children }: { children: ReactNode }) => { update({ name: newName, }) + await changeNickname(newName) } return ( diff --git a/src/auth.ts b/src/auth.ts index 5d803b7..ed384f1 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -2,10 +2,10 @@ import NextAuth from 'next-auth' import Kakao from 'next-auth/providers/kakao' -import { login } from './lib/api' -import { changeNickname } from './lib/api/user' +import { login, refreshAT } from './lib/api/auth' -export const { handlers, signIn, signOut, auth } = NextAuth({ +/* eslint-disable-next-line @typescript-eslint/naming-convention */ +export const { handlers, signIn, signOut, auth, unstable_update } = NextAuth({ providers: [ Kakao({ clientId: process.env.AUTH_KAKAO_ID, @@ -23,16 +23,18 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ if (account && user) { try { // 신규 유저인지 확인, polabo 백에서 토큰 발급 - const { newUser, nickName, accessToken } = await login({ - email: user.email!, - nickName: user.name!, - birthDt: '2024-08-11', // TODO: 기획 대기 - gender: 'F', // TODO: 기획 대기 - }) - + const { newUser, nickName, accessToken, refreshToken, expiredDate } = + await login({ + email: user.email!, + nickName: user.name!, + birthDt: '2024-08-11', // TODO: 기획 대기 + gender: 'F', // TODO: 기획 대기 + }) user.name = nickName user.newUser = newUser user.accessToken = accessToken + user.refreshToken = refreshToken + user.expiredDate = expiredDate } catch (e) { console.log('error', e) return false @@ -41,30 +43,45 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ return true }, - async jwt({ token, user, trigger, session }) { + async jwt({ token, user, account, trigger, session }) { if (trigger === 'update' && session?.name) { const { name } = session - - token.name = name // client update - await changeNickname(name) // server update + token.name = name + } + if (trigger === 'update' && session?.accessToken) { + token.accessToken = session.accessToken + token.refreshToken = session.refreshToken + token.expiredDate = session.expiredDate } - if (user) { + if (user && account) { + // first time login return { ...token, - accessToken: user.accessToken, newUser: user.newUser, + accessToken: user.accessToken, + refreshToken: user.refreshToken, + expiredDate: user.expiredDate, + user, } } - return token + if (Date.now() < new Date(token.expiredDate).getTime()) { + // AT not expired + return token + } + // AT expired - update token + const newToken = await refreshAT(token.refreshToken) + return { ...token, ...newToken } }, async session({ session, token }) { - return { - ...session, - accessToken: token.accessToken, - newUser: token.newUser, + if (token) { + session.accessToken = token.accessToken + session.refreshToken = token.refreshToken + session.expiredDate = token.expiredDate + session.newUser = token.newUser } + return session }, }, }) diff --git a/src/components/HamburgerMenu/Drawer.tsx b/src/components/HamburgerMenu/Drawer.tsx index 022e655..b485e3f 100644 --- a/src/components/HamburgerMenu/Drawer.tsx +++ b/src/components/HamburgerMenu/Drawer.tsx @@ -87,7 +87,7 @@ const Drawer = ({ children, isOpen, onClose }: DrawerProps) => { return isOpen ? ReactDOM.createPortal( -
+
{ + const text = await res.text() + + if (!res.ok) { + throw new Error( + `Request failed: ${res.status} - ${res.statusText} - ${text || 'No error message provided'}`, + ) + } + + if (!text) { + throw new Error('No response body') + } + + try { + return JSON.parse(text) + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + throw new Error(`Failed to parse JSON: ${error.message}`) + } +} export const login = async (body: SignInPayload): Promise => { - const res = await post('/api/v1/oauth/sign-in', { + const res = await fetch(`${process.env.API_HOST}/api/v1/oauth/sign-in`, { + method: 'POST', body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, }) - return res.data + const data = await handleResponse(res) + return data.data +} + +export const refreshAT = async (refreshToken: string) => { + const res = await fetch(`${process.env.API_HOST}/api/v1/oauth/re-issue`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${refreshToken}`, + }, + }) + + const data = await handleResponse(res) + + return { + accessToken: data.data.accessToken, + refreshToken: data.data.refreshToken, + expiredDate: data.data.expiredDate, + } } diff --git a/src/lib/api/base.ts b/src/lib/api/base.ts index 843bdec..eeaa33f 100644 --- a/src/lib/api/base.ts +++ b/src/lib/api/base.ts @@ -1,4 +1,14 @@ +'use server' + +import { auth, unstable_update as update, signOut } from '@/auth' import { RequestInit } from 'next/dist/server/web/spec-extension/request' +import { refreshAT } from './auth' + +const ERRORS = { + FETCH_FAILED: '데이터를 불러오는데 실패했습니다.', + SAVE_FAILED: '데이터를 저장하는데 실패했습니다.', + DELETE_FAILED: '데이터를 삭제하는데 실패했습니다.', +} const getBaseUrl = (useMocked: boolean) => useMocked ? 'http://localhost:3001' : process.env.API_HOST @@ -9,20 +19,65 @@ const fetchApi = async ( useMocked: boolean, errorMessage: string, ) => { - const res = await fetch(getBaseUrl(useMocked) + path, options) + const session = await auth() + + const fetchOptions = { + ...options, + headers: { + 'content-type': 'application/json', + ...options?.headers, + ...(session && { Authorization: `Bearer ${session?.accessToken}` }), + }, + } + + let res = await fetch(getBaseUrl(useMocked) + path, fetchOptions) + + if (res.status === 401 && session) { + const resJson = await res.json() + if (resJson.code === 'JWT002') { + // AT expired + try { + const newToken = await refreshAT(session.refreshToken) + + await update({ + accessToken: newToken.accessToken, + refreshToken: newToken.refreshToken, + expiredDate: newToken.expiredDate, + }) + + // retry original request + res = await fetch(getBaseUrl(useMocked) + path, fetchOptions) + } catch (e) { + signOut() + } + } else { + // RT expired or invalid + signOut() + } + } if (!res.ok) { - throw new Error(errorMessage) + throw new Error(`Error: ${res.status} - ${res.statusText}. ${errorMessage}`) } - return res.json() + const text = await res.text() + return text ? JSON.parse(text) : null } export const get = async ( path: string, options: RequestInit = {}, useMocked = false, -) => fetchApi(path, options, useMocked, '데이터를 불러오는데 실패했습니다.') +) => + fetchApi( + path, + { + ...options, + method: 'GET', + }, + useMocked, + ERRORS.FETCH_FAILED, + ) export const post = async ( path: string, @@ -34,10 +89,9 @@ export const post = async ( { ...options, method: 'POST', - headers: { 'Content-Type': 'application/json' }, }, useMocked, - '데이터를 저장하는데 실패했습니다.', + ERRORS.SAVE_FAILED, ) export const put = async ( @@ -50,10 +104,9 @@ export const put = async ( { ...options, method: 'PUT', - headers: { 'Content-Type': 'application/json' }, }, useMocked, - '데이터를 저장하는데 실패했습니다.', + ERRORS.SAVE_FAILED, ) export const deleteApi = async ( @@ -66,8 +119,7 @@ export const deleteApi = async ( { ...options, method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, }, useMocked, - '데이터를 삭제하는데 실패했습니다.', + ERRORS.DELETE_FAILED, ) diff --git a/src/lib/api/user.ts b/src/lib/api/user.ts index a64176e..387e5ab 100644 --- a/src/lib/api/user.ts +++ b/src/lib/api/user.ts @@ -1,5 +1,5 @@ -import { put } from '@/lib/api/base' import { WithdrawUserPayload } from '@/types' +import { get, put } from './base' export const withdraw = async (body: WithdrawUserPayload) => { return put('/api/v1/user/withdraw', { @@ -8,7 +8,15 @@ export const withdraw = async (body: WithdrawUserPayload) => { } export const changeNickname = async (nickName: string) => { - return put('/api/v1/user/nickname', { + return put(`/api/v1/user/nickname`, { body: JSON.stringify({ nickName }), }) } + +export const viewProfile = async () => { + return get('/api/v1/user/profile', { + next: { + tags: ['profile'], + }, + }) +} diff --git a/src/types/auth.ts b/src/types/auth.ts index b982998..6e8d8cf 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -9,5 +9,6 @@ export interface User { newUser: boolean nickName: string accessToken: string + expiredDate: string refreshToken: string } diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index 2b2fa58..ce93c36 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -5,6 +5,8 @@ declare module 'next-auth/jwt' { interface JWT { newUser: boolean accessToken: string + refreshToken: string + expiredDate: string } } @@ -12,9 +14,13 @@ declare module 'next-auth' { interface Session extends DefaultSession { newUser: boolean accessToken: string + refreshToken: string + expiredDate: string } interface User { newUser: boolean accessToken: string + refreshToken: string + expiredDate: string } }