Skip to content

Commit

Permalink
Merge pull request #83 from DDD-Community/feature/82
Browse files Browse the repository at this point in the history
[FEATURE/82] auth fetch
  • Loading branch information
hwanheejung authored Aug 14, 2024
2 parents 9015870 + c2669fa commit e65bb87
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 39 deletions.
4 changes: 2 additions & 2 deletions src/app/(board)/board/create/components/BoardNameForm.tsx
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/app/(onboarding)/signup/components/NicknameForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -18,6 +19,7 @@ const NicknameForm = () => {
update({
name: nickname,
})
await changeNickname(nickname)
router.push('/signup/complete')
}

Expand Down
2 changes: 2 additions & 0 deletions src/app/mypage/profileEdit/components/NicknameForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -15,6 +16,7 @@ const NicknameForm = ({ children }: { children: ReactNode }) => {
update({
name: newName,
})
await changeNickname(newName)
}

return (
Expand Down
59 changes: 38 additions & 21 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
},
},
})
2 changes: 1 addition & 1 deletion src/components/HamburgerMenu/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const Drawer = ({ children, isOpen, onClose }: DrawerProps) => {

return isOpen
? ReactDOM.createPortal(
<div className="fixed left-0 right-0 top-0 mx-auto flex h-dvh max-w-md flex-col overflow-hidden">
<div className="fixed left-0 right-0 top-0 z-20 mx-auto flex h-dvh max-w-md flex-col overflow-hidden">
<DrawerContext.Provider value={context}>
<DrawerOverlay closeOnClick>
<div
Expand Down
49 changes: 46 additions & 3 deletions src/lib/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,53 @@
import { SignInPayload, User } from '@/types'
import { post } from './base'

const handleResponse = async (res: Response) => {
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<User> => {
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,
}
}
72 changes: 62 additions & 10 deletions src/lib/api/base.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -34,10 +89,9 @@ export const post = async (
{
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
},
useMocked,
'데이터를 저장하는데 실패했습니다.',
ERRORS.SAVE_FAILED,
)

export const put = async (
Expand All @@ -50,10 +104,9 @@ export const put = async (
{
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
},
useMocked,
'데이터를 저장하는데 실패했습니다.',
ERRORS.SAVE_FAILED,
)

export const deleteApi = async (
Expand All @@ -66,8 +119,7 @@ export const deleteApi = async (
{
...options,
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
},
useMocked,
'데이터를 삭제하는데 실패했습니다.',
ERRORS.DELETE_FAILED,
)
12 changes: 10 additions & 2 deletions src/lib/api/user.ts
Original file line number Diff line number Diff line change
@@ -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', {
Expand All @@ -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'],
},
})
}
1 change: 1 addition & 0 deletions src/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export interface User {
newUser: boolean
nickName: string
accessToken: string
expiredDate: string
refreshToken: string
}
6 changes: 6 additions & 0 deletions src/types/next-auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ declare module 'next-auth/jwt' {
interface JWT {
newUser: boolean
accessToken: string
refreshToken: string
expiredDate: string
}
}

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
}
}

0 comments on commit e65bb87

Please sign in to comment.