Files
dyolink/frontend/src/lib/api/client.ts

87 lines
2.6 KiB
TypeScript
Raw Normal View History

// src/lib/api/client.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import type { ApiError } from '@/types/api';
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
}
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true, // ✅ REQUIRED FOR COOKIES
headers: {
'Content-Type': 'application/json',
},
timeout: 10000,
});
/** Invitation preview/accept must work with no cookies (public API, no JWT). */
function isPublicInvitationRequest(url: string | undefined): boolean {
if (!url) return false;
return (
url.includes('/staff/invitations/preview') ||
url.includes('/staff/invitations/accept') ||
url.includes('/organizations/invitations/preview') ||
url.includes('/organizations/invitations/accept')
);
}
/** Session bootstrap / auth endpoints where 401 means "not logged in", not "retry refresh". */
function shouldSkipRefreshRetry(url: string | undefined): boolean {
if (!url) return false;
return (
url.includes('/auth/profile') ||
url.includes('/auth/refresh') ||
url.includes('/auth/login') ||
url.includes('/auth/register') ||
url.includes('/auth/logout') ||
url.includes('/auth/forgot-password') ||
url.includes('/auth/profile/password')
);
}
// ❌ REMOVE request interceptor completely (no Authorization header)
// ✅ Response interceptor
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as CustomAxiosRequestConfig;
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!isPublicInvitationRequest(originalRequest.url) &&
!shouldSkipRefreshRetry(originalRequest.url)
) {
originalRequest._retry = true;
try {
// ✅ refresh via cookie (no body needed ideally)
await axios.post(
`${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`,
{},
{ withCredentials: true }
);
return apiClient(originalRequest);
} catch (refreshError) {
if (typeof window !== 'undefined') {
return Promise.reject(error); // ✅ just fail silently
}
return Promise.reject(refreshError);
}
}
const apiError: ApiError = {
statusCode: error.response?.status || 500,
message:
(error.response?.data as any)?.message ||
error.message ||
'An unexpected error occurred',
error: (error.response?.data as any)?.error,
};
return Promise.reject(apiError);
}
);