feat(documents): implement document upload functionality for companies

- Added DocumentItem component to display uploaded documents in CompanyDetailsPage.
- Introduced useUploadCompanyDocuments hook for handling document uploads.
- Updated CreateCompany form to include document upload capabilities and validation for file types and sizes.
- Enhanced API services to support batch document uploads and retrieval of document information.
- Improved response handling in the file service for better error management and user feedback.
This commit is contained in:
AmirReza Jamali
2026-06-06 17:18:02 +03:30
parent 88f223bf08
commit da1e2f89e7
19 changed files with 1038 additions and 429 deletions
+179 -131
View File
@@ -1,160 +1,208 @@
import { AxiosError, AxiosResponse } from "axios";
import {
AxiosError,
AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
import Cookies from "js-cookie";
import { toast } from "react-toastify";
import { getLogoutCallback } from "../contexts/User.ctx";
import { API_ENDPOINTS, api } from "../lib/api";
import { COOKIE_KEYS } from "../lib/shared/cookies";
let isRefreshing = false;
let failedQueue: any[] = [];
const SIGN_IN_PATH = "/auth/sign-in";
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach((prom) => {
type RetriableRequest = InternalAxiosRequestConfig & { _retry?: boolean };
interface TokenPair {
access_token: string;
refresh_token: string;
}
interface ApiErrorData {
message?: string;
error?: {
code?: string;
message?: string;
details?: string;
};
}
interface QueuedRequest {
resolve: (token: string) => void;
reject: (reason?: unknown) => void;
}
// ---------------------------------------------------------------------------
// Refresh-token queue: requests that arrive mid-refresh wait here and replay
// once a fresh access token is available (or fail together if refresh fails).
// ---------------------------------------------------------------------------
let isRefreshing = false;
let failedQueue: QueuedRequest[] = [];
const processQueue = (error: unknown, token: string | null = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
reject(error);
} else if (token) {
resolve(token);
}
});
failedQueue = [];
};
export const responseInterceptor = (response: AxiosResponse) => {
return response;
const queueRequest = (
originalRequest: RetriableRequest,
): Promise<AxiosResponse> =>
new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
});
// ---------------------------------------------------------------------------
// Session / auth helpers
// ---------------------------------------------------------------------------
const clearLocalSession = () => {
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
if (typeof window !== "undefined") {
localStorage.removeItem("auth-token");
localStorage.removeItem("user-data");
}
};
export const responseErrorHandler = async (error: AxiosError) => {
const originalRequest = error.config as any;
const redirectToSignIn = () => {
if (typeof window !== "undefined") {
window.location.href = SIGN_IN_PATH;
}
};
// Skip token refresh for login endpoint - let login errors pass through
const isLoginRequest = originalRequest?.url?.includes(
API_ENDPOINTS.USER.LOGIN,
);
/** Logs the user out via the registered callback, falling back to a manual reset. */
const forceLogout = () => {
const logout = getLogoutCallback();
if (logout) {
logout();
return;
}
const isRefreshTokenRequest = originalRequest?.url?.includes(
API_ENDPOINTS.USER.REFRESH_TOKEN,
);
clearLocalSession();
redirectToSignIn();
};
// If we get a 401 on refresh token endpoint, logout immediately (token refresh failed)
if (error.response?.status === 401 && isRefreshTokenRequest) {
const logout = getLogoutCallback();
if (logout) {
logout();
} else {
// Fallback if logout callback is not registered
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
if (typeof window !== "undefined") {
window.location.href = "/auth/sign-in";
}
// ---------------------------------------------------------------------------
// Error reporting
// ---------------------------------------------------------------------------
/**
* Builds the message shown in the error toast, appending the API-provided
* `details` (e.g. field-level validation hints) to the main error message
* when available.
*/
const buildErrorMessage = (data: unknown, fallback: string): string => {
const payload = data as ApiErrorData | undefined;
const message = payload?.error?.message || payload?.message || fallback;
const details = payload?.error?.details;
return details ? `${message}: ${details}` : message;
};
const notifyError = (error: unknown) => {
const data = (error as AxiosError).response?.data;
toast.error(buildErrorMessage(data, (error as Error).message));
};
// ---------------------------------------------------------------------------
// Token refresh
// ---------------------------------------------------------------------------
const requestNewAccessToken = async (refreshToken: string): Promise<string> => {
const response = await api.post(API_ENDPOINTS.USER.REFRESH_TOKEN, {
refresh_token: refreshToken,
});
const { access_token, refresh_token } = response.data.data as TokenPair;
if (!access_token || !refresh_token) {
throw new Error("Invalid token received from refresh endpoint.");
}
Cookies.set(COOKIE_KEYS.access_token, access_token);
Cookies.set(COOKIE_KEYS.refresh_token, refresh_token);
return access_token;
};
/**
* Handles a 401 by refreshing the access token and replaying the request.
* Concurrent requests are queued until the in-flight refresh settles.
*/
const handleUnauthorized = async (originalRequest: RetriableRequest) => {
if (isRefreshing) {
return queueRequest(originalRequest);
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = Cookies.get(COOKIE_KEYS.refresh_token);
if (!refreshToken) {
forceLogout();
return Promise.reject(new Error("Refresh token not found."));
}
const accessToken = await requestNewAccessToken(refreshToken);
processQueue(null, accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
forceLogout();
notifyError(refreshError);
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
};
// ---------------------------------------------------------------------------
// Request predicates
// ---------------------------------------------------------------------------
const matchesEndpoint = (request: RetriableRequest | undefined, endpoint: string) =>
request?.url?.includes(endpoint) ?? false;
// Login errors must surface to the caller instead of triggering a refresh.
const isLoginRequest = (request?: RetriableRequest) =>
matchesEndpoint(request, API_ENDPOINTS.USER.LOGIN);
const isRefreshTokenRequest = (request?: RetriableRequest) =>
matchesEndpoint(request, API_ENDPOINTS.USER.REFRESH_TOKEN);
// ---------------------------------------------------------------------------
// Interceptors
// ---------------------------------------------------------------------------
export const responseInterceptor = (response: AxiosResponse) => response;
export const responseErrorHandler = (error: AxiosError) => {
const originalRequest = error.config as RetriableRequest | undefined;
const isUnauthorized = error.response?.status === 401;
// A 401 from the refresh endpoint itself means the session is unrecoverable.
if (isUnauthorized && isRefreshTokenRequest(originalRequest)) {
forceLogout();
return Promise.reject(error);
}
if (
error.response?.status === 401 &&
isUnauthorized &&
originalRequest &&
!originalRequest._retry &&
!isLoginRequest
!isLoginRequest(originalRequest)
) {
if (isRefreshing) {
return new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token: string) => {
originalRequest.headers["Authorization"] = "Bearer " + token;
return api(originalRequest);
})
.catch((err) => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = Cookies.get(COOKIE_KEYS.refresh_token);
if (!refreshToken) {
// No refresh token available, logout immediately
const logout = getLogoutCallback();
if (logout) {
logout();
} else {
// Fallback if logout callback is not registered
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
if (typeof window !== "undefined") {
window.location.href = "/auth/sign-in";
}
}
return Promise.reject(new Error("Refresh token not found."));
}
const response = await api.post(API_ENDPOINTS.USER.REFRESH_TOKEN, {
refresh_token: refreshToken,
});
const { access_token, refresh_token } = response.data.data as {
access_token: string;
refresh_token: string;
};
if (!access_token || !refresh_token) {
throw new Error("Invalid token received from refresh endpoint.");
}
Cookies.set(COOKIE_KEYS.access_token, access_token);
Cookies.set(COOKIE_KEYS.refresh_token, refresh_token);
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${access_token}`;
}
processQueue(null, access_token);
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// Use logout callback from User context
const logout = getLogoutCallback();
if (logout) {
logout();
} else {
// Fallback if logout callback is not registered
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
if (typeof window !== "undefined") {
localStorage.removeItem("auth-token");
localStorage.removeItem("user-data");
window.location.href = "/auth/sign-in";
}
}
const errorMessage =
(
(refreshError as AxiosError).response?.data as {
message: string;
error: { code: string; message: string };
}
)?.error?.message || (refreshError as Error).message;
toast.error(errorMessage);
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
return handleUnauthorized(originalRequest);
}
const errorMessage =
(
error.response?.data as {
message: string;
error: { code: string; message: string };
}
)?.error?.message || error.message;
toast.error(errorMessage);
notifyError(error);
return Promise.reject(error);
};