diff --git a/packages/fetcher/src/factories.ts b/packages/fetcher/src/factories.ts index 6c5e8a118b..92620915fc 100644 --- a/packages/fetcher/src/factories.ts +++ b/packages/fetcher/src/factories.ts @@ -2,7 +2,11 @@ import { GetServerSidePropsContext } from 'next' import { generateUrl, parseUrl } from '@titicaca/view-utilities' import { HttpResponse, RequestOptions } from './types' -import { captureHttpError } from './response-handler' +import { + captureHttpError, + handle401Error, + NEED_REFRESH_IDENTIFIER, +} from './response-handler' export type BaseFetcher = < SuccessBody, @@ -140,10 +144,11 @@ export function authFetcherize( return firstTrialResponse } - const { status: firstTrialResponseStatus } = - firstTrialResponse as HttpResponse - - if (firstTrialResponseStatus !== 401) { + const checkFirstTrialResponse = await handle401Error< + SuccessBody, + FailureBody + >(firstTrialResponse as HttpResponse) + if (checkFirstTrialResponse !== NEED_REFRESH_IDENTIFIER) { return firstTrialResponse } diff --git a/packages/fetcher/src/fetcher.ts b/packages/fetcher/src/fetcher.ts index 4d2322c0bd..f5db0a0041 100644 --- a/packages/fetcher/src/fetcher.ts +++ b/packages/fetcher/src/fetcher.ts @@ -83,7 +83,7 @@ function makeFetchRetryable({ } } -function readResponseBody(response: Response) { +export function readResponseBody(response: Response) { const contentType = response.headers.get('content-type') const jsonParseAvailable = contentType && /json/.test(contentType) diff --git a/packages/fetcher/src/index.ts b/packages/fetcher/src/index.ts index 171fe3b333..09acd3e825 100644 --- a/packages/fetcher/src/index.ts +++ b/packages/fetcher/src/index.ts @@ -2,10 +2,15 @@ export * from './types' export { authFetcherize, ssrFetcherize } from './factories' export { NEED_LOGIN_IDENTIFIER } from './factories' export { addFetchersToGssp } from './add-fetchers-to-gssp' -export { fetcher } from './fetcher' +export { fetcher, readResponseBody } from './fetcher' export { get, put, post, del } from './methods' export { authGuardedFetchers } from './auth-guarded-methods' -export { captureHttpError } from './response-handler' +export { + captureHttpError, + handle401Error, + NEED_REFRESH_IDENTIFIER, + ACCESS_TOKEN_EXPIRED_EXCEPTION, +} from './response-handler' export { sessionRefresh, sessionRefreshOnSSR, diff --git a/packages/fetcher/src/response-handler.ts b/packages/fetcher/src/response-handler.ts index 9092dabef1..a7e2efc26f 100644 --- a/packages/fetcher/src/response-handler.ts +++ b/packages/fetcher/src/response-handler.ts @@ -1,6 +1,8 @@ import { withScope, captureException } from '@sentry/nextjs' import { HttpResponse } from './types' +import { NEED_LOGIN_IDENTIFIER } from './factories' +import { readResponseBody } from './fetcher' export function captureHttpError< Response extends HttpResponse, @@ -13,3 +15,42 @@ export function captureHttpError< }) } } + +export const ACCESS_TOKEN_EXPIRED_EXCEPTION = 'AccessTokenExpiredException' +export const NEED_REFRESH_IDENTIFIER = 'NEED_REFRESH' + +interface ErrorResponseBody { + exception: string + message: string + status: string +} + +type ResponseWithError = Pick & { + ok: false + parsedBody: ErrorResponseBody +} + +export async function handle401Error( + response: HttpResponse | Response, +) { + if (response.ok || response.status !== 401) { + return response + } + + let exception = '' + + if (response instanceof Response) { + const parsedBody = (await readResponseBody(response)) as ErrorResponseBody + exception = parsedBody.exception + } else { + const errorResponse = response as ResponseWithError + if (errorResponse.status === 401) { + exception = errorResponse.parsedBody.exception + } + } + + if (exception === ACCESS_TOKEN_EXPIRED_EXCEPTION) { + return NEED_REFRESH_IDENTIFIER + } + return NEED_LOGIN_IDENTIFIER +} diff --git a/packages/react-contexts/src/middlewares/refresh-session.ts b/packages/react-contexts/src/middlewares/refresh-session.ts index 87ffc0de0c..d03216370f 100644 --- a/packages/react-contexts/src/middlewares/refresh-session.ts +++ b/packages/react-contexts/src/middlewares/refresh-session.ts @@ -5,7 +5,12 @@ import { NextRequest, NextResponse, } from 'next/server' -import { get, post } from '@titicaca/fetcher' +import { + get, + post, + handle401Error, + NEED_REFRESH_IDENTIFIER, +} from '@titicaca/fetcher' import { parseString, splitCookiesString } from 'set-cookie-parser' import { TP_SE, @@ -59,10 +64,14 @@ export function refreshSessionMiddleware(next: NextMiddleware) { * 401 : TP_SE가 유효하지 않고 TP_TK가 유효한 경우 * 403 : TP_TK가 모두 유효하지 않은 경우 */ + const firstTrialResponse = await get< + unknown, + { status: number; exception: string; message: string } + >('/api/users/session/verify', options) - const firstTrialResponse = await get('/api/users/session/verify', options) + const checkFirstTrialResponse = await handle401Error(firstTrialResponse) - if (firstTrialResponse.status !== 401) { + if (checkFirstTrialResponse !== NEED_REFRESH_IDENTIFIER) { const setCookie = firstTrialResponse.headers.get('set-cookie') if (setCookie) { const setCookies = splitCookiesString(setCookie) diff --git a/packages/react-contexts/src/session-context/provider/hooks.ts b/packages/react-contexts/src/session-context/provider/hooks.ts index c6e9528d85..7a4ca7573f 100644 --- a/packages/react-contexts/src/session-context/provider/hooks.ts +++ b/packages/react-contexts/src/session-context/provider/hooks.ts @@ -48,7 +48,10 @@ export function useLogout({ const redirectUrl = getRedirectUrl(redirectLocation) window.location.href = redirectUrl + return } + + window.location.reload() }, [clearUserState]) if (type === 'app') { diff --git a/packages/review/src/data/graphql/client.ts b/packages/review/src/data/graphql/client.ts index 7a90e6e010..368f463956 100644 --- a/packages/review/src/data/graphql/client.ts +++ b/packages/review/src/data/graphql/client.ts @@ -1,5 +1,8 @@ import { ClientError, request } from 'graphql-request' -import { sessionRefresh } from '@titicaca/fetcher' +import { + sessionRefresh, + ACCESS_TOKEN_EXPIRED_EXCEPTION, +} from '@titicaca/fetcher' import { Requester, getSdk } from './generated' @@ -18,10 +21,12 @@ export async function reviewClient(query: () => Promise) { return response } catch (e) { if (e instanceof ClientError && e.response.status === 401) { - const refreshResponse = await sessionRefresh({}) - if (refreshResponse) { - const newResponse = await query() - return newResponse + if (e.response.exception === ACCESS_TOKEN_EXPIRED_EXCEPTION) { + const refreshResponse = await sessionRefresh({}) + if (refreshResponse) { + const newResponse = await query() + return newResponse + } } } throw e diff --git a/packages/standard-action-handler/package.json b/packages/standard-action-handler/package.json index e9bc2d057f..227ff19dfa 100644 --- a/packages/standard-action-handler/package.json +++ b/packages/standard-action-handler/package.json @@ -38,6 +38,7 @@ ] }, "dependencies": { + "@titicaca/fetcher": "workspace:*", "@titicaca/modals": "workspace:*", "@titicaca/router": "workspace:*", "@titicaca/scroll-to-element": "workspace:*", diff --git a/packages/standard-action-handler/src/converse.tsx b/packages/standard-action-handler/src/converse.tsx index 1c72e95061..ca9e4f78fa 100644 --- a/packages/standard-action-handler/src/converse.tsx +++ b/packages/standard-action-handler/src/converse.tsx @@ -1,6 +1,7 @@ import qs from 'qs' import { createRoot } from 'react-dom/client' import { Modal } from '@titicaca/modals' +import { authGuardedFetchers, NEED_LOGIN_IDENTIFIER } from '@titicaca/fetcher' import { ContextOptions, WebActionParams } from './types' @@ -92,15 +93,17 @@ export function OpenModal({ async function fetchApi( url: string, ): Promise<{ type: ModalType; title: string; description: string }> { - const response = await fetch(url, { - method: 'POST', + const response = await authGuardedFetchers.post< + { title: string; description: string }, + unknown + >(url, { headers: { 'Content-Type': 'application/json', }, }) - if (!response.ok) { - if (response.status === 400 || response.status === 401) { + if (response === NEED_LOGIN_IDENTIFIER || !response.ok) { + if (response === NEED_LOGIN_IDENTIFIER) { return NEED_LOGIN_CONTENT } return { @@ -110,10 +113,7 @@ async function fetchApi( '서비스 이용이 원활하지 않습니다.\n잠시 후 다시 이용해 주세요.', } } else { - const { title, description } = (await response.json()) as { - title: string - description: string - } + const { title, description } = response.parsedBody return { type: 'normal', title, description } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40c97e27c9..bb69cfabfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1611,6 +1611,9 @@ importers: packages/standard-action-handler: dependencies: + '@titicaca/fetcher': + specifier: workspace:* + version: link:../fetcher '@titicaca/modals': specifier: workspace:* version: link:../modals