cleanup
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useMemo, useReducer } from 'react';
|
||||
import { clearAuthToken, setAuthToken } from '../lib/apiClient';
|
||||
import { clearAuthToken, setAuthToken, setAuthRefreshHandlers } from '../lib/apiClient';
|
||||
import { ApiProvider } from './ApiContext';
|
||||
import { listTenants } from '../lib/apiClient';
|
||||
|
||||
@@ -209,6 +209,21 @@ const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }
|
||||
}
|
||||
}, [state.tenant]);
|
||||
|
||||
useEffect(() => {
|
||||
setAuthRefreshHandlers({
|
||||
onRefreshSuccess: (token, payload) => {
|
||||
dispatch({ type: 'TOKEN_REFRESH_SUCCESS', token, tenant: payload?.tenant ?? null });
|
||||
},
|
||||
onRefreshFailure: (error) => {
|
||||
dispatch({ type: 'TOKEN_REFRESH_FAILURE', error: (error as Error)?.message || null });
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
setAuthRefreshHandlers({});
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
let abort = false;
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ const useDocumentPreview = ({
|
||||
|
||||
const existing = documentLinks.get(documentId);
|
||||
const now = Date.now();
|
||||
const expiresAt = Number.isFinite(existing?.expiresAt) ? Number(existing?.expiresAt) : null;
|
||||
const expiresAt = existing?.expiresAt ?? null;
|
||||
if (!force && existing && (!expiresAt || expiresAt > now)) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -184,9 +184,7 @@ export const resolveDocumentAssetUrl = (
|
||||
const view = createAssetView(asset);
|
||||
const object = view.getPrimaryObject();
|
||||
const url = object?.url || view.getPrimaryUrl();
|
||||
const expiresAt = Number.isFinite(object?.expires_at)
|
||||
? Number(object?.expires_at)
|
||||
: resolveAssetExpiresAt(asset);
|
||||
const expiresAt = object?.expires_at ?? resolveAssetExpiresAt(asset);
|
||||
const now = Date.now();
|
||||
if (url && (!expiresAt || expiresAt > now)) {
|
||||
return url;
|
||||
@@ -242,7 +240,7 @@ class AssetManager {
|
||||
const isPrimarySatisfied = () => {
|
||||
const object = view.getObject(1);
|
||||
if (object?.url) {
|
||||
const objectExpiresAt = Number.isFinite(object.expires_at) ? Number(object.expires_at) : null;
|
||||
const objectExpiresAt = object.expires_at ?? null;
|
||||
if (!objectExpiresAt || objectExpiresAt > now) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
const extraSummaryRows = useMemo(() => {
|
||||
const rows: DocumentSummaryRow[] = [];
|
||||
const currentVersionNumber = document?.current_version?.version_number;
|
||||
if (Number.isFinite(currentVersionNumber)) {
|
||||
if (currentVersionNumber != null) {
|
||||
rows.push({
|
||||
key: 'current-version',
|
||||
label: 'Current version',
|
||||
|
||||
@@ -349,9 +349,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
||||
|
||||
const documentCount = documentIdList.length;
|
||||
const folderCount = folderIdList.length;
|
||||
const totalCount = Number.isFinite(selectionCount)
|
||||
? Number(selectionCount)
|
||||
: documentCount + folderCount;
|
||||
const totalCount = selectionCount ?? documentCount + folderCount;
|
||||
|
||||
const selectedDocuments = useMemo<DocumentLike[]>(() => {
|
||||
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
TenantSnippet,
|
||||
TagResponse,
|
||||
} from './apiTypes';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import type { AxiosError, AxiosInstance, InternalAxiosRequestConfig, AxiosRequestConfig } from 'axios';
|
||||
|
||||
export const httpClient: Pick<AxiosInstance, 'get' | 'post' | 'patch' | 'delete' | 'defaults'> = {
|
||||
get: api.get.bind(api),
|
||||
@@ -22,6 +22,22 @@ export const httpClient: Pick<AxiosInstance, 'get' | 'post' | 'patch' | 'delete'
|
||||
defaults: api.defaults,
|
||||
};
|
||||
|
||||
type AuthAwareRequestConfig = InternalAxiosRequestConfig & {
|
||||
_retry?: boolean;
|
||||
skipAuthRefresh?: boolean;
|
||||
};
|
||||
|
||||
type AuthRequestConfig = AxiosRequestConfig & {
|
||||
skipAuthRefresh?: boolean;
|
||||
};
|
||||
type AuthRefreshHandlers = {
|
||||
onRefreshSuccess?: (token: string, payload?: { tenant?: unknown }) => void;
|
||||
onRefreshFailure?: (error: unknown) => void;
|
||||
};
|
||||
|
||||
let refreshPromise: Promise<string> | null = null;
|
||||
let authRefreshHandlers: AuthRefreshHandlers = {};
|
||||
|
||||
const normalizeNumber = (value: unknown): number | undefined => {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
@@ -168,7 +184,7 @@ export const performLogin = async (payload: Record<string, unknown>): Promise<un
|
||||
};
|
||||
|
||||
export const refreshSession = async (): Promise<{ access_token?: string; tenant?: unknown }> => {
|
||||
const { data } = await api.post('/auth/refresh');
|
||||
const { data } = await api.post('/auth/refresh', undefined, { skipAuthRefresh: true } as AuthRequestConfig);
|
||||
return data as { access_token?: string; tenant?: unknown };
|
||||
};
|
||||
|
||||
@@ -277,6 +293,59 @@ export const clearAuthToken = () => {
|
||||
delete api.defaults.headers.common.Authorization;
|
||||
};
|
||||
|
||||
export const setAuthRefreshHandlers = (handlers: AuthRefreshHandlers) => {
|
||||
authRefreshHandlers = handlers;
|
||||
};
|
||||
|
||||
const performTokenRefresh = async (): Promise<string> => {
|
||||
if (refreshPromise) {
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
refreshPromise = refreshSession()
|
||||
.then((data) => {
|
||||
const token = data?.access_token;
|
||||
if (!token) {
|
||||
throw new Error('Missing access token in refresh response');
|
||||
}
|
||||
setAuthToken(token);
|
||||
authRefreshHandlers.onRefreshSuccess?.(token, { tenant: data?.tenant });
|
||||
return token;
|
||||
})
|
||||
.catch((error) => {
|
||||
authRefreshHandlers.onRefreshFailure?.(error);
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
refreshPromise = null;
|
||||
});
|
||||
|
||||
return refreshPromise;
|
||||
};
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const response = error.response;
|
||||
const config = (error.config || {}) as AuthAwareRequestConfig;
|
||||
if (!response || response.status !== 401 || config._retry || config.skipAuthRefresh) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
config._retry = true;
|
||||
|
||||
try {
|
||||
const token = await performTokenRefresh();
|
||||
const headers = (config.headers ?? {}) as Record<string, unknown>;
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
config.headers = headers as AuthAwareRequestConfig['headers'];
|
||||
return api(config);
|
||||
} catch (refreshError) {
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export type {
|
||||
DownloadLink,
|
||||
DocumentResponse,
|
||||
|
||||
Reference in New Issue
Block a user