cleanup
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, { useContext, useEffect, useMemo, useReducer } from 'react';
|
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 { ApiProvider } from './ApiContext';
|
||||||
import { listTenants } from '../lib/apiClient';
|
import { listTenants } from '../lib/apiClient';
|
||||||
|
|
||||||
@@ -209,6 +209,21 @@ const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }
|
|||||||
}
|
}
|
||||||
}, [state.tenant]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
let abort = false;
|
let abort = false;
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const useDocumentPreview = ({
|
|||||||
|
|
||||||
const existing = documentLinks.get(documentId);
|
const existing = documentLinks.get(documentId);
|
||||||
const now = Date.now();
|
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)) {
|
if (!force && existing && (!expiresAt || expiresAt > now)) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,9 +184,7 @@ export const resolveDocumentAssetUrl = (
|
|||||||
const view = createAssetView(asset);
|
const view = createAssetView(asset);
|
||||||
const object = view.getPrimaryObject();
|
const object = view.getPrimaryObject();
|
||||||
const url = object?.url || view.getPrimaryUrl();
|
const url = object?.url || view.getPrimaryUrl();
|
||||||
const expiresAt = Number.isFinite(object?.expires_at)
|
const expiresAt = object?.expires_at ?? resolveAssetExpiresAt(asset);
|
||||||
? Number(object?.expires_at)
|
|
||||||
: resolveAssetExpiresAt(asset);
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (url && (!expiresAt || expiresAt > now)) {
|
if (url && (!expiresAt || expiresAt > now)) {
|
||||||
return url;
|
return url;
|
||||||
@@ -242,7 +240,7 @@ class AssetManager {
|
|||||||
const isPrimarySatisfied = () => {
|
const isPrimarySatisfied = () => {
|
||||||
const object = view.getObject(1);
|
const object = view.getObject(1);
|
||||||
if (object?.url) {
|
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) {
|
if (!objectExpiresAt || objectExpiresAt > now) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -495,7 +495,7 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
|||||||
const extraSummaryRows = useMemo(() => {
|
const extraSummaryRows = useMemo(() => {
|
||||||
const rows: DocumentSummaryRow[] = [];
|
const rows: DocumentSummaryRow[] = [];
|
||||||
const currentVersionNumber = document?.current_version?.version_number;
|
const currentVersionNumber = document?.current_version?.version_number;
|
||||||
if (Number.isFinite(currentVersionNumber)) {
|
if (currentVersionNumber != null) {
|
||||||
rows.push({
|
rows.push({
|
||||||
key: 'current-version',
|
key: 'current-version',
|
||||||
label: 'Current version',
|
label: 'Current version',
|
||||||
|
|||||||
@@ -349,9 +349,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
|||||||
|
|
||||||
const documentCount = documentIdList.length;
|
const documentCount = documentIdList.length;
|
||||||
const folderCount = folderIdList.length;
|
const folderCount = folderIdList.length;
|
||||||
const totalCount = Number.isFinite(selectionCount)
|
const totalCount = selectionCount ?? documentCount + folderCount;
|
||||||
? Number(selectionCount)
|
|
||||||
: documentCount + folderCount;
|
|
||||||
|
|
||||||
const selectedDocuments = useMemo<DocumentLike[]>(() => {
|
const selectedDocuments = useMemo<DocumentLike[]>(() => {
|
||||||
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
|
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type {
|
|||||||
TenantSnippet,
|
TenantSnippet,
|
||||||
TagResponse,
|
TagResponse,
|
||||||
} from './apiTypes';
|
} 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'> = {
|
export const httpClient: Pick<AxiosInstance, 'get' | 'post' | 'patch' | 'delete' | 'defaults'> = {
|
||||||
get: api.get.bind(api),
|
get: api.get.bind(api),
|
||||||
@@ -22,6 +22,22 @@ export const httpClient: Pick<AxiosInstance, 'get' | 'post' | 'patch' | 'delete'
|
|||||||
defaults: api.defaults,
|
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 normalizeNumber = (value: unknown): number | undefined => {
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
return Number.isFinite(n) ? n : undefined;
|
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 }> => {
|
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 };
|
return data as { access_token?: string; tenant?: unknown };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -277,6 +293,59 @@ export const clearAuthToken = () => {
|
|||||||
delete api.defaults.headers.common.Authorization;
|
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 {
|
export type {
|
||||||
DownloadLink,
|
DownloadLink,
|
||||||
DocumentResponse,
|
DocumentResponse,
|
||||||
|
|||||||
Reference in New Issue
Block a user