Files
sam-react-prod/src/lib/api/authenticated-fetch.ts

96 lines
3.4 KiB
TypeScript
Raw Normal View History

/**
* Authenticated Fetch Gateway
*
* API .
* 401 refresh (globalThis ) retry .
*
* :
* -
* - 401 refreshAccessToken (globalThis )
* -
*
* ( ):
* - (PROXY: request.cookies / Server Actions: cookies() API)
* - (PROXY: Set-Cookie / Server Actions: cookies() API)
* - (Server Actions: redirect('/login'))
* - ( )
*/
import { refreshAccessToken, type RefreshResult } from './refresh-token';
export type AuthenticatedFetchResult = {
/** 백엔드 응답 (성공이든 실패든) */
response: Response;
/** refresh 성공 시 새 토큰 (호출자가 쿠키에 저장) */
newTokens?: RefreshResult;
/** true면 인증 실패 (호출자가 쿠키 삭제 + 리다이렉트 처리) */
authFailed?: boolean;
};
/**
*
*
* :
* - { response } (401 )
* - { response, newTokens } 401 refresh
* - { response, authFailed: true } (refresh // )
*
* @param url API URL
* @param options fetch ( Authorization )
* @param refreshToken refresh_token ( 401 )
* @param caller (: 'PROXY' | 'serverFetch' | 'ServerApiClient')
*/
export async function authenticatedFetch(
url: string,
options: RequestInit,
refreshToken: string | undefined,
caller: string = 'unknown'
): Promise<AuthenticatedFetchResult> {
// 1. 요청 실행 (호출자가 이미 모든 헤더 설정)
const response = await fetch(url, options);
// 2. 401이 아니면 그대로 반환
if (response.status !== 401) {
return { response };
}
// 3. 401이지만 refresh_token 없음 → 인증 실패
if (!refreshToken) {
console.warn(`🔴 [${caller}] 401 (no refresh token)`);
return { response, authFailed: true };
}
// 4. 401 + refresh_token 있음 → 갱신 시도 (globalThis 캐시로 중복 방지)
console.log(`🔄 [${caller}] Got 401, attempting token refresh...`);
const refreshResult = await refreshAccessToken(refreshToken, caller);
if (!refreshResult.success || !refreshResult.accessToken) {
console.warn(`🔴 [${caller}] Token refresh failed`);
return { response, authFailed: true };
}
// 5. 새 토큰으로 재시도
console.log(`✅ [${caller}] Token refreshed, retrying...`);
const retryHeaders = new Headers(options.headers || {});
retryHeaders.set('Authorization', `Bearer ${refreshResult.accessToken}`);
const retryResponse = await fetch(url, {
...options,
headers: retryHeaders,
});
console.log(`🔵 [${caller}] Retry status: ${retryResponse.status}`);
// 6. 재시도도 401 → 인증 실패
if (retryResponse.status === 401) {
console.warn(`🔴 [${caller}] Retry still 401, auth failed`);
return { response: retryResponse, authFailed: true };
}
// 7. 재시도 성공 → 새 토큰과 함께 반환 (호출자가 쿠키 설정)
return {
response: retryResponse,
newTokens: refreshResult,
};
}