From e0dfdbf571f69c7eafff1f21543b7f53e931ac92 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 16 Jul 2024 08:10:39 +0000 Subject: [PATCH] refactor: Add refreshToken function for handling token refresh --- app/business/services/user/user.command.ts | 36 +++++++++++++++++++++- middleware.ts | 29 ++++++++++++++--- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/app/business/services/user/user.command.ts b/app/business/services/user/user.command.ts index 95e63182..ab168ffa 100644 --- a/app/business/services/user/user.command.ts +++ b/app/business/services/user/user.command.ts @@ -5,7 +5,12 @@ import { API_PATH } from '../../api-path'; import { SignUpRequestBody, SignInRequestBody, ValidateTokenResponse, UserDeleteRequestBody } from './user.type'; import { httpErrorHandler } from '@/app/utils/http/http-error-handler'; import { BadRequestError, UnauthorizedError } from '@/app/utils/http/http-error'; -import { SignUpFormSchema, SignInFormSchema, SignInResponseSchema } from './user.validation'; +import { + SignUpFormSchema, + SignInFormSchema, + SignInResponseSchema, + ValidateTokenResponseSchema, +} from './user.validation'; import { cookies } from 'next/headers'; import { isValidation } from '@/app/utils/zod/validation.util'; import { redirect } from 'next/navigation'; @@ -123,6 +128,35 @@ export async function authenticate(prevState: FormState, formData: FormData): Pr redirect('/my'); } +export async function refreshToken(): Promise { + const refreshToken = cookies().get('refreshToken')?.value; + try { + const response = await fetch(`${API_PATH.auth}/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ refreshToken }), + }); + + const result = await response.json(); + + httpErrorHandler(response, result); + + if (isValidation(result, ValidateTokenResponseSchema)) { + return result; + } else { + throw 'Invalid token response schema.'; + } + } catch (error) { + if (error instanceof BadRequestError) { + return false; + } else { + throw error; + } + } +} + export async function createUser(prevState: FormState, formData: FormData): Promise { const validatedFields = SignUpFormSchema.safeParse({ authId: formData.get('authId'), diff --git a/middleware.ts b/middleware.ts index 06e20dd2..f4ece991 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,9 +1,10 @@ -import type { NextRequest } from 'next/server'; +import { NextResponse, type NextRequest } from 'next/server'; import { auth } from './app/business/services/user/user.query'; import { isInitUser } from './app/business/services/user/user.validation'; +import { refreshToken } from './app/business/services/user/user.command'; async function getAuth(request: NextRequest): Promise<{ - role: 'guest' | 'user' | 'init'; + role: 'guest' | 'expired' | 'user' | 'init'; }> { const accessToken = request.cookies.get('accessToken')?.value; if (!accessToken) { @@ -15,10 +16,8 @@ async function getAuth(request: NextRequest): Promise<{ const user = await auth(); if (!user) { - request.cookies.delete('accessToken'); - request.cookies.delete('refreshToken'); return { - role: 'guest', + role: 'expired', }; } @@ -42,6 +41,10 @@ function isAllowedGuestPath(path: string, strict: boolean = false) { export async function middleware(request: NextRequest) { const auth = await getAuth(request); + if (auth.role === 'expired') { + return await retryAuth(request); + } + if (auth.role === 'init' && !request.nextUrl.pathname.startsWith('/grade-upload')) { return Response.redirect(new URL('/grade-upload', request.url)); } @@ -55,6 +58,22 @@ export async function middleware(request: NextRequest) { } } +async function retryAuth(request: NextRequest) { + const response = NextResponse.redirect(request.url); + const result = await refreshToken(); + if (result === false) { + response.cookies.delete('accessToken'); + response.cookies.delete('refreshToken'); + } else { + response.cookies.set('accessToken', result.accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + path: '/', + }); + } + return response; +} + export const config = { matcher: ['/((?!api|mockServiceWorker|_next/static|_next/image|.*\\.png$).*)'], };