Skip to content
This repository was archived by the owner on Jan 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions packages/fetcher/src/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Extending = unknown> = <
SuccessBody,
Expand Down Expand Up @@ -140,10 +144,11 @@ export function authFetcherize<Fetcher extends BaseFetcher>(
return firstTrialResponse
}

const { status: firstTrialResponseStatus } =
firstTrialResponse as HttpResponse<SuccessBody, FailureBody>

if (firstTrialResponseStatus !== 401) {
const checkFirstTrialResponse = await handle401Error<
SuccessBody,
FailureBody
>(firstTrialResponse as HttpResponse<SuccessBody, FailureBody>)
if (checkFirstTrialResponse !== NEED_REFRESH_IDENTIFIER) {
return firstTrialResponse
}

Expand Down
2 changes: 1 addition & 1 deletion packages/fetcher/src/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
9 changes: 7 additions & 2 deletions packages/fetcher/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions packages/fetcher/src/response-handler.ts
Original file line number Diff line number Diff line change
@@ -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<unknown, unknown>,
Expand All @@ -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<Response, 'headers' | 'ok' | 'status' | 'url'> & {
ok: false
parsedBody: ErrorResponseBody
}

export async function handle401Error<SuccessBody, FailureBody>(
response: HttpResponse<SuccessBody, FailureBody> | 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
}
15 changes: 12 additions & 3 deletions packages/react-contexts/src/middlewares/refresh-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions packages/react-contexts/src/session-context/provider/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export function useLogout({
const redirectUrl = getRedirectUrl(redirectLocation)

window.location.href = redirectUrl
return
}

window.location.reload()
}, [clearUserState])

if (type === 'app') {
Expand Down
15 changes: 10 additions & 5 deletions packages/review/src/data/graphql/client.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -18,10 +21,12 @@ export async function reviewClient<T>(query: () => Promise<T>) {
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
Expand Down
1 change: 1 addition & 0 deletions packages/standard-action-handler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
]
},
"dependencies": {
"@titicaca/fetcher": "workspace:*",
"@titicaca/modals": "workspace:*",
"@titicaca/router": "workspace:*",
"@titicaca/scroll-to-element": "workspace:*",
Expand Down
16 changes: 8 additions & 8 deletions packages/standard-action-handler/src/converse.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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 {
Expand All @@ -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 }
}
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading