Files
sam-react-prod/src/lib/api/refresh-token.ts

143 lines
4.2 KiB
TypeScript
Raw Normal View History

/**
* 🔄 Refresh Token
*
* (/api/proxy) serverFetch
*
* 문제: useEffect에서 API refresh_token
* - refresh_token ( )
* - refresh_token ( )
*
* 해결: 5초간 refresh
* -
* - refresh Promise도
* - serverFetch가
*/
export type RefreshResult = {
success: boolean;
accessToken?: string;
refreshToken?: string;
expiresIn?: number;
};
// 캐시 상태 (모듈 레벨에서 공유)
let refreshCache: {
promise: Promise<RefreshResult> | null;
timestamp: number;
result: RefreshResult | null;
} = {
promise: null,
timestamp: 0,
result: null,
};
const REFRESH_CACHE_TTL = 5000; // 5초
/**
* ( )
*/
async function doRefreshToken(refreshToken: string): Promise<RefreshResult> {
try {
const refreshUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/refresh`;
console.log('🔄 [RefreshToken] Refresh request:', {
url: refreshUrl,
hasApiKey: !!process.env.API_KEY,
refreshTokenLength: refreshToken?.length,
});
const response = await fetch(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
if (!response.ok) {
const errorBody = await response.text();
console.warn('🔴 [RefreshToken] Token refresh failed:', {
status: response.status,
statusText: response.statusText,
body: errorBody,
});
return { success: false };
}
const data = await response.json();
console.log('✅ [RefreshToken] Token refreshed successfully');
return {
success: true,
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
};
} catch (error) {
console.error('🔴 [RefreshToken] Token refresh error:', error);
return { success: false };
}
}
/**
* (5 )
*
* :
* 1.
* 2. refresh가 Promise를
* 3. refresh
*
* @param refreshToken - refresh_token
* @param caller - (: 'PROXY' | 'serverFetch')
*/
export async function refreshAccessToken(
refreshToken: string,
caller: string = 'unknown'
): Promise<RefreshResult> {
const now = Date.now();
// 1. 캐시된 성공 결과가 유효하면 즉시 반환
if (refreshCache.result && refreshCache.result.success && now - refreshCache.timestamp < REFRESH_CACHE_TTL) {
console.log(`🔵 [${caller}] Using cached refresh result (age: ${now - refreshCache.timestamp}ms)`);
return refreshCache.result;
}
// 2. 진행 중인 refresh가 있고, 아직 결과가 없으면 기다림 (실패한 결과는 캐시 안 함)
if (refreshCache.promise && !refreshCache.result && now - refreshCache.timestamp < REFRESH_CACHE_TTL) {
console.log(`🔵 [${caller}] Waiting for ongoing refresh...`);
return refreshCache.promise;
}
// 2-1. 이전 refresh가 실패했으면 캐시 초기화
if (refreshCache.result && !refreshCache.result.success) {
console.log(`🔄 [${caller}] Previous refresh failed, clearing cache...`);
refreshCache.promise = null;
refreshCache.result = null;
}
// 3. 새 refresh 시작
console.log(`🔄 [${caller}] Starting new refresh request...`);
refreshCache.timestamp = now;
refreshCache.result = null;
refreshCache.promise = doRefreshToken(refreshToken).then(result => {
refreshCache.result = result;
return result;
});
return refreshCache.promise;
}
/**
* ()
*/
export function clearRefreshCache(): void {
refreshCache = {
promise: null,
timestamp: 0,
result: null,
};
}