ai-shit
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { httpClient, setAuthToken, clearAuthToken } from '../lib/apiClient';
|
||||||
|
|
||||||
|
type HttpClient = typeof httpClient;
|
||||||
|
|
||||||
|
interface ApiContextValue {
|
||||||
|
client: HttpClient;
|
||||||
|
setAuthToken: (token?: string | null) => void;
|
||||||
|
clearAuthToken: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiContext = createContext<ApiContextValue | null>(null);
|
||||||
|
|
||||||
|
export const ApiProvider: React.FC<PropsWithChildren<{ initialToken?: string | null }>> = ({
|
||||||
|
initialToken = null,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialToken) {
|
||||||
|
setAuthToken(initialToken);
|
||||||
|
}
|
||||||
|
}, [initialToken]);
|
||||||
|
|
||||||
|
const value = useMemo<ApiContextValue>(
|
||||||
|
() => ({
|
||||||
|
client: httpClient,
|
||||||
|
setAuthToken,
|
||||||
|
clearAuthToken,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useApi = (): ApiContextValue => {
|
||||||
|
const ctx = useContext(ApiContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('useApi must be used within an ApiProvider');
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHttpClient = (): HttpClient => httpClient;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useContext, useEffect, useMemo, useReducer } from 'react';
|
import React, { useContext, useEffect, useMemo, useReducer } from 'react';
|
||||||
import api from '../lib/api';
|
import { clearAuthToken, setAuthToken } from '../lib/apiClient';
|
||||||
|
import { ApiProvider } from './ApiContext';
|
||||||
import { listTenants } from '../lib/apiClient';
|
import { listTenants } from '../lib/apiClient';
|
||||||
|
|
||||||
type Tenant = Record<string, unknown> | null;
|
type Tenant = Record<string, unknown> | null;
|
||||||
@@ -61,7 +62,7 @@ if (storage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (STORED_TOKEN) {
|
if (STORED_TOKEN) {
|
||||||
api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`;
|
setAuthToken(STORED_TOKEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialAppState: AppState = {
|
const initialAppState: AppState = {
|
||||||
@@ -188,10 +189,10 @@ const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = state.token ?? '';
|
const token = state.token ?? '';
|
||||||
if (token) {
|
if (token) {
|
||||||
api.defaults.headers.common.Authorization = `Bearer ${token}`;
|
setAuthToken(token);
|
||||||
storage?.setItem('papercrate_token', token);
|
storage?.setItem('papercrate_token', token);
|
||||||
} else {
|
} else {
|
||||||
delete api.defaults.headers.common.Authorization;
|
clearAuthToken();
|
||||||
storage?.removeItem('papercrate_token');
|
storage?.removeItem('papercrate_token');
|
||||||
}
|
}
|
||||||
}, [state.token]);
|
}, [state.token]);
|
||||||
@@ -242,11 +243,13 @@ const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }
|
|||||||
const stateValue = useMemo(() => state, [state]);
|
const stateValue = useMemo(() => state, [state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppStateContext.Provider value={stateValue}>
|
<ApiProvider initialToken={state.token}>
|
||||||
<AppDispatchContext.Provider value={dispatch}>
|
<AppStateContext.Provider value={stateValue}>
|
||||||
{children}
|
<AppDispatchContext.Provider value={dispatch}>
|
||||||
</AppDispatchContext.Provider>
|
{children}
|
||||||
</AppStateContext.Provider>
|
</AppDispatchContext.Provider>
|
||||||
|
</AppStateContext.Provider>
|
||||||
|
</ApiProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,4 +269,4 @@ const useAppDispatch = (): React.Dispatch<AppAction> => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { api, AppStateProvider, useAppState, useAppDispatch };
|
export { AppStateProvider, useAppState, useAppDispatch };
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { AxiosInstance } from 'axios';
|
|
||||||
|
|
||||||
export type Identifier = string | number;
|
export type Identifier = string | number;
|
||||||
|
|
||||||
type Nullable<T> = T | null;
|
type Nullable<T> = T | null;
|
||||||
@@ -205,20 +203,20 @@ export const resolveDocumentAssetUrl = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
class AssetManager {
|
class AssetManager {
|
||||||
api: AxiosInstance | null;
|
fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null;
|
||||||
assetPresignTtlMs: number;
|
assetPresignTtlMs: number;
|
||||||
assetCache: Map<Identifier, AssetLike>;
|
assetCache: Map<Identifier, AssetLike>;
|
||||||
assetInflight: Map<string, Promise<AssetLike | null>>;
|
assetInflight: Map<string, Promise<AssetLike | null>>;
|
||||||
|
|
||||||
constructor({ api, assetPresignTtlMs }: { api: AxiosInstance | null; assetPresignTtlMs: number }) {
|
constructor({ fetchAsset, assetPresignTtlMs }: { fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null; assetPresignTtlMs: number }) {
|
||||||
this.api = api;
|
this.fetchAsset = fetchAsset;
|
||||||
this.assetPresignTtlMs = assetPresignTtlMs;
|
this.assetPresignTtlMs = assetPresignTtlMs;
|
||||||
this.assetCache = new Map();
|
this.assetCache = new Map();
|
||||||
this.assetInflight = new Map();
|
this.assetInflight = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
setApi(api: AxiosInstance | null) {
|
setFetchAsset(fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null) {
|
||||||
this.api = api;
|
this.fetchAsset = fetchAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
rememberAsset(entry?: Nullable<AssetLike>) {
|
rememberAsset(entry?: Nullable<AssetLike>) {
|
||||||
@@ -271,18 +269,16 @@ class AssetManager {
|
|||||||
return this.assetInflight.get(inflightKey);
|
return this.assetInflight.get(inflightKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.api) {
|
if (!this.fetchAsset) {
|
||||||
return Promise.reject(new Error('AssetManager API client is not configured.'));
|
return Promise.reject(new Error('AssetManager fetcher is not configured.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const request: Promise<AssetLike | null> = this.api
|
const request: Promise<AssetLike | null> = this.fetchAsset(asset.id)
|
||||||
.get(`/assets/${asset.id}`)
|
.then((data) => {
|
||||||
.then(({ data }) => {
|
if (!data) return null;
|
||||||
const cachedEntry = this.assetCache.get(asset.id) || baseAsset;
|
const cachedEntry = this.assetCache.get(asset.id) || baseAsset;
|
||||||
const combined = { ...cachedEntry, ...asset, ...data };
|
const combined = { ...cachedEntry, ...asset, ...data };
|
||||||
const expires_at =
|
const expires_at = resolveAssetExpiresAt(combined);
|
||||||
resolveAssetExpiresAt(data)
|
|
||||||
?? resolveAssetExpiresAt(combined);
|
|
||||||
const entry = {
|
const entry = {
|
||||||
...combined,
|
...combined,
|
||||||
url: resolveAssetUrl(combined),
|
url: resolveAssetUrl(combined),
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import type { MutableRefObject } from 'react';
|
import type { MutableRefObject } from 'react';
|
||||||
import type { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
|
import { clearAuthToken, logoutSession, refreshSession, setAuthToken } from '../../lib/apiClient';
|
||||||
import { AxiosHeaders } from 'axios';
|
|
||||||
|
|
||||||
type AppStatus = string;
|
type AppStatus = string;
|
||||||
|
|
||||||
type AppDispatch = (action: { type: string; [key: string]: unknown }) => void;
|
type AppDispatch = (action: { type: string; [key: string]: unknown }) => void;
|
||||||
|
|
||||||
type NotifyApiError = (error: unknown, fallbackMessage: string, variant?: string) => void;
|
|
||||||
|
|
||||||
type SetStatusMessage = (message: string, variant?: string) => void;
|
type SetStatusMessage = (message: string, variant?: string) => void;
|
||||||
|
|
||||||
type SetLoading = (state: boolean) => void;
|
type SetLoading = (state: boolean) => void;
|
||||||
|
|
||||||
interface RetryableAxiosRequestConfig extends InternalAxiosRequestConfig {
|
|
||||||
_retry?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseAuthManagerArgs {
|
interface UseAuthManagerArgs {
|
||||||
apiClient: AxiosInstance;
|
|
||||||
token?: string | null;
|
token?: string | null;
|
||||||
appStatus: AppStatus;
|
appStatus: AppStatus;
|
||||||
appDispatch: AppDispatch;
|
appDispatch: AppDispatch;
|
||||||
notifyApiError: NotifyApiError;
|
|
||||||
setStatusMessage: SetStatusMessage;
|
setStatusMessage: SetStatusMessage;
|
||||||
setLoading: SetLoading;
|
setLoading: SetLoading;
|
||||||
}
|
}
|
||||||
@@ -33,40 +24,23 @@ interface UseAuthManagerResult {
|
|||||||
handleLogout: () => Promise<void>;
|
handleLogout: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureAxiosHeaders = (
|
|
||||||
headers?: InternalAxiosRequestConfig['headers'],
|
|
||||||
): AxiosHeaders => {
|
|
||||||
if (headers instanceof AxiosHeaders) {
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
return AxiosHeaders.from(headers || {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setHeaderAuthorization = (config: InternalAxiosRequestConfig, token: string): void => {
|
|
||||||
const headers = ensureAxiosHeaders(config.headers);
|
|
||||||
headers.set('Authorization', `Bearer ${token}`);
|
|
||||||
config.headers = headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useAuthManager = ({
|
const useAuthManager = ({
|
||||||
apiClient,
|
|
||||||
token,
|
token,
|
||||||
appStatus,
|
appStatus,
|
||||||
appDispatch,
|
appDispatch,
|
||||||
notifyApiError,
|
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
setLoading,
|
setLoading,
|
||||||
}: UseAuthManagerArgs): UseAuthManagerResult => {
|
}: UseAuthManagerArgs): UseAuthManagerResult => {
|
||||||
const tokenRef = useRef<string | null>(token);
|
const tokenRef = useRef<string | null>(token);
|
||||||
const refreshPromiseRef = useRef<Promise<string> | null>(null);
|
|
||||||
const initialRefreshAttemptedRef = useRef(Boolean(token));
|
const initialRefreshAttemptedRef = useRef(Boolean(token));
|
||||||
|
|
||||||
const refreshAccessToken = useCallback(async (): Promise<string> => {
|
const refreshAccessToken = useCallback(async (): Promise<string> => {
|
||||||
console.log('[Auth] Attempting to refresh access token…');
|
console.log('[Auth] Attempting to refresh access token…');
|
||||||
appDispatch({ type: 'TOKEN_REFRESH_START' });
|
appDispatch({ type: 'TOKEN_REFRESH_START' });
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.post<{ access_token?: string; tenant?: unknown }>('/auth/refresh');
|
const data = await refreshSession();
|
||||||
if (data?.access_token) {
|
if (data?.access_token) {
|
||||||
|
setAuthToken(data.access_token);
|
||||||
appDispatch({
|
appDispatch({
|
||||||
type: 'TOKEN_REFRESH_SUCCESS',
|
type: 'TOKEN_REFRESH_SUCCESS',
|
||||||
token: data.access_token,
|
token: data.access_token,
|
||||||
@@ -81,7 +55,7 @@ const useAuthManager = ({
|
|||||||
appDispatch({ type: 'TOKEN_REFRESH_FAILURE', error: (error as Error)?.message || null });
|
appDispatch({ type: 'TOKEN_REFRESH_FAILURE', error: (error as Error)?.message || null });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [apiClient, appDispatch]);
|
}, [appDispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tokenRef.current = token;
|
tokenRef.current = token;
|
||||||
@@ -95,90 +69,19 @@ const useAuthManager = ({
|
|||||||
}
|
}
|
||||||
}, [token, appStatus, refreshAccessToken]);
|
}, [token, appStatus, refreshAccessToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const requestInterceptor = apiClient.interceptors.request.use((config) => {
|
|
||||||
const currentToken = tokenRef.current;
|
|
||||||
if (currentToken) {
|
|
||||||
const headers = ensureAxiosHeaders(config.headers);
|
|
||||||
if (!headers.has('Authorization')) {
|
|
||||||
headers.set('Authorization', `Bearer ${currentToken}`);
|
|
||||||
}
|
|
||||||
config.headers = headers;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseInterceptor = apiClient.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
async (error) => {
|
|
||||||
const axiosError = error as AxiosError & { config?: RetryableAxiosRequestConfig };
|
|
||||||
const { response, config } = axiosError;
|
|
||||||
if (!response || !config) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = response.status;
|
|
||||||
const url = String(config?.url ?? '');
|
|
||||||
const isAuthRoute = url.includes('/auth/login') || url.includes('/auth/refresh');
|
|
||||||
|
|
||||||
if (status === 401 && !config._retry && !isAuthRoute) {
|
|
||||||
console.warn('[Auth] 401 received for', url, '- attempting token refresh');
|
|
||||||
|
|
||||||
if (!refreshPromiseRef.current) {
|
|
||||||
refreshPromiseRef.current = (async () => {
|
|
||||||
try {
|
|
||||||
return await refreshAccessToken();
|
|
||||||
} finally {
|
|
||||||
refreshPromiseRef.current = null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newToken = await refreshPromiseRef.current;
|
|
||||||
if (!newToken) {
|
|
||||||
throw new Error('No token returned from refresh');
|
|
||||||
}
|
|
||||||
config._retry = true;
|
|
||||||
setHeaderAuthorization(config, newToken);
|
|
||||||
console.log('[Auth] Retrying original request', url);
|
|
||||||
try {
|
|
||||||
return await apiClient(config);
|
|
||||||
} catch (retryError) {
|
|
||||||
if ((retryError as AxiosError)?.response?.status === 401) {
|
|
||||||
notifyApiError(retryError, 'Session expired. Please log in again.');
|
|
||||||
}
|
|
||||||
throw retryError;
|
|
||||||
}
|
|
||||||
} catch (refreshError) {
|
|
||||||
console.warn('[Auth] Refresh failed, clearing session');
|
|
||||||
notifyApiError(refreshError, 'Session expired. Please log in again.');
|
|
||||||
return Promise.reject(refreshError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
apiClient.interceptors.request.eject(requestInterceptor);
|
|
||||||
apiClient.interceptors.response.eject(responseInterceptor);
|
|
||||||
};
|
|
||||||
}, [apiClient, notifyApiError, refreshAccessToken]);
|
|
||||||
|
|
||||||
const handleLogout = useCallback(async () => {
|
const handleLogout = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await apiClient.post('/auth/logout');
|
await logoutSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Auth] Failed to revoke refresh token during logout', error);
|
console.warn('[Auth] Failed to revoke refresh token during logout', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
clearAuthToken();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
appDispatch({ type: 'LOGOUT' });
|
appDispatch({ type: 'LOGOUT' });
|
||||||
setStatusMessage('Logged out.', 'info');
|
setStatusMessage('Logged out.', 'info');
|
||||||
}
|
}
|
||||||
}, [apiClient, appDispatch, setLoading, setStatusMessage]);
|
}, [appDispatch, setLoading, setStatusMessage]);
|
||||||
|
|
||||||
return { tokenRef, refreshAccessToken, handleLogout };
|
return { tokenRef, refreshAccessToken, handleLogout };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import useApiError from '../useApiError';
|
|||||||
import TagManager from '../../tag_manager';
|
import TagManager from '../../tag_manager';
|
||||||
import usePasskeys from '../../settings/usePasskeys';
|
import usePasskeys from '../../settings/usePasskeys';
|
||||||
import { useManagementModals } from '../../app/useManagementModals';
|
import { useManagementModals } from '../../app/useManagementModals';
|
||||||
import { api, useAppDispatch, useAppState } from '../../app/appState';
|
import { useAppDispatch, useAppState } from '../../app/appState';
|
||||||
|
import { fetchAsset } from '../../lib/apiClient';
|
||||||
|
import { useApi } from '../../app/ApiContext';
|
||||||
import useWorkspaceSelection from '../../app/useWorkspaceSelection';
|
import useWorkspaceSelection from '../../app/useWorkspaceSelection';
|
||||||
import { useEntryPointer as useEntryPointerCore } from '../../documents/useEntryPointer';
|
import { useEntryPointer as useEntryPointerCore } from '../../documents/useEntryPointer';
|
||||||
import { isTagTransferEvent } from '../../documents/tagTransfer';
|
import { isTagTransferEvent } from '../../documents/tagTransfer';
|
||||||
@@ -153,6 +155,7 @@ const useDocumentsWorkspace = ({
|
|||||||
tenant,
|
tenant,
|
||||||
tenants: tenantOptionsRaw = [],
|
tenants: tenantOptionsRaw = [],
|
||||||
} = appState;
|
} = appState;
|
||||||
|
const { client: apiClient } = useApi();
|
||||||
|
|
||||||
const tenantRecord = (tenant ?? null) as TenantOption | null;
|
const tenantRecord = (tenant ?? null) as TenantOption | null;
|
||||||
const tenantNameCandidate = tenantRecord?.name ?? tenantRecord?.slug ?? null;
|
const tenantNameCandidate = tenantRecord?.name ?? tenantRecord?.slug ?? null;
|
||||||
@@ -179,11 +182,9 @@ const useDocumentsWorkspace = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [creatingFolder, setCreatingFolder] = useState(false);
|
const [creatingFolder, setCreatingFolder] = useState(false);
|
||||||
const { tokenRef, handleLogout } = useAuthManager({
|
const { tokenRef, handleLogout } = useAuthManager({
|
||||||
apiClient: api,
|
|
||||||
token,
|
token,
|
||||||
appStatus,
|
appStatus,
|
||||||
appDispatch,
|
appDispatch,
|
||||||
notifyApiError,
|
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
setLoading,
|
setLoading,
|
||||||
});
|
});
|
||||||
@@ -220,7 +221,11 @@ const useDocumentsWorkspace = ({
|
|||||||
const shellRef = useRef(null);
|
const shellRef = useRef(null);
|
||||||
const assetManagerRef = useRef(null);
|
const assetManagerRef = useRef(null);
|
||||||
if (!assetManagerRef.current) {
|
if (!assetManagerRef.current) {
|
||||||
assetManagerRef.current = new AssetManager({ api, assetPresignTtlMs: ASSET_PRESIGN_TTL_MS });
|
const fetcher = async (id: Identifier) => {
|
||||||
|
const asset = await fetchAsset(id);
|
||||||
|
return (asset as unknown) as any;
|
||||||
|
};
|
||||||
|
assetManagerRef.current = new AssetManager({ fetchAsset: fetcher, assetPresignTtlMs: ASSET_PRESIGN_TTL_MS });
|
||||||
}
|
}
|
||||||
const assetManager = assetManagerRef.current;
|
const assetManager = assetManagerRef.current;
|
||||||
|
|
||||||
@@ -346,7 +351,7 @@ const useDocumentsWorkspace = ({
|
|||||||
isInvalidFolderDrop,
|
isInvalidFolderDrop,
|
||||||
} = useFolderTree({
|
} = useFolderTree({
|
||||||
initialSelectedFolder: routeFolderId || 'root',
|
initialSelectedFolder: routeFolderId || 'root',
|
||||||
apiClient: api,
|
apiClient,
|
||||||
tenantIdRef,
|
tenantIdRef,
|
||||||
documentsSortFieldRef: activeSortFieldRef,
|
documentsSortFieldRef: activeSortFieldRef,
|
||||||
documentsSortDirectionRef: activeSortDirectionRef,
|
documentsSortDirectionRef: activeSortDirectionRef,
|
||||||
@@ -369,7 +374,7 @@ const useDocumentsWorkspace = ({
|
|||||||
isFilterActive,
|
isFilterActive,
|
||||||
documentsFilterValue,
|
documentsFilterValue,
|
||||||
} = useDocumentsSearch({
|
} = useDocumentsSearch({
|
||||||
api,
|
api: apiClient,
|
||||||
token,
|
token,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
navigate,
|
navigate,
|
||||||
@@ -505,7 +510,7 @@ const useDocumentsWorkspace = ({
|
|||||||
handleTagDelete,
|
handleTagDelete,
|
||||||
setTags,
|
setTags,
|
||||||
} = useTags({
|
} = useTags({
|
||||||
apiClient: api,
|
apiClient,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
tagManager,
|
tagManager,
|
||||||
@@ -549,7 +554,7 @@ const useDocumentsWorkspace = ({
|
|||||||
handleCorrespondentDelete,
|
handleCorrespondentDelete,
|
||||||
setCorrespondents,
|
setCorrespondents,
|
||||||
} = useCorrespondents({
|
} = useCorrespondents({
|
||||||
apiClient: api,
|
apiClient,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
tenantIdRef,
|
tenantIdRef,
|
||||||
@@ -604,7 +609,7 @@ const useDocumentsWorkspace = ({
|
|||||||
handleBulkTagRemoveFromDetail,
|
handleBulkTagRemoveFromDetail,
|
||||||
handleBulkSelectionReanalyze,
|
handleBulkSelectionReanalyze,
|
||||||
} = useDocumentTagging({
|
} = useDocumentTagging({
|
||||||
apiClient: api,
|
apiClient,
|
||||||
tags,
|
tags,
|
||||||
tagManager,
|
tagManager,
|
||||||
refreshTags,
|
refreshTags,
|
||||||
@@ -623,7 +628,7 @@ const useDocumentsWorkspace = ({
|
|||||||
clearUploadQueue,
|
clearUploadQueue,
|
||||||
resetUploadsState,
|
resetUploadsState,
|
||||||
} = useDocumentUploads({
|
} = useDocumentUploads({
|
||||||
apiClient: api,
|
apiClient,
|
||||||
token,
|
token,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
@@ -660,7 +665,7 @@ const useDocumentsWorkspace = ({
|
|||||||
handleCorrespondentRemove,
|
handleCorrespondentRemove,
|
||||||
handleCorrespondentAdd,
|
handleCorrespondentAdd,
|
||||||
} = useDocumentCorrespondentActions({
|
} = useDocumentCorrespondentActions({
|
||||||
apiClient: api,
|
apiClient,
|
||||||
correspondents,
|
correspondents,
|
||||||
handleCorrespondentCreate,
|
handleCorrespondentCreate,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
@@ -1369,7 +1374,7 @@ const useDocumentsWorkspace = ({
|
|||||||
}, [missingBreadcrumbAncestors, ensureFolderData]);
|
}, [missingBreadcrumbAncestors, ensureFolderData]);
|
||||||
|
|
||||||
const { handleTenantSelect } = useTenantManager({
|
const { handleTenantSelect } = useTenantManager({
|
||||||
apiClient: api,
|
apiClient,
|
||||||
appDispatch,
|
appDispatch,
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
resetWorkspaceState,
|
resetWorkspaceState,
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import type {
|
|||||||
TenantSnippet,
|
TenantSnippet,
|
||||||
TagResponse,
|
TagResponse,
|
||||||
} from './apiTypes';
|
} from './apiTypes';
|
||||||
|
import type { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
|
export const httpClient: Pick<AxiosInstance, 'get' | 'post' | 'patch' | 'delete' | 'defaults'> = {
|
||||||
|
get: api.get.bind(api),
|
||||||
|
post: api.post.bind(api),
|
||||||
|
patch: api.patch.bind(api),
|
||||||
|
delete: api.delete.bind(api),
|
||||||
|
defaults: api.defaults,
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeNumber = (value: unknown): number | undefined => {
|
const normalizeNumber = (value: unknown): number | undefined => {
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
@@ -158,6 +167,15 @@ export const performLogin = async (payload: Record<string, unknown>): Promise<un
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const refreshSession = async (): Promise<{ access_token?: string; tenant?: unknown }> => {
|
||||||
|
const { data } = await api.post('/auth/refresh');
|
||||||
|
return data as { access_token?: string; tenant?: unknown };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutSession = async (): Promise<void> => {
|
||||||
|
await api.post('/auth/logout');
|
||||||
|
};
|
||||||
|
|
||||||
export const selectTenant = async (
|
export const selectTenant = async (
|
||||||
payload: { tenant_id: Identifier },
|
payload: { tenant_id: Identifier },
|
||||||
selectionToken: string,
|
selectionToken: string,
|
||||||
@@ -247,6 +265,18 @@ export const listTenants = async (): Promise<TenantSnippet[]> => {
|
|||||||
return Array.isArray(data?.tenants) ? data.tenants : [];
|
return Array.isArray(data?.tenants) ? data.tenants : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setAuthToken = (token?: string | null) => {
|
||||||
|
if (token) {
|
||||||
|
api.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||||
|
} else {
|
||||||
|
delete api.defaults.headers.common.Authorization;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearAuthToken = () => {
|
||||||
|
delete api.defaults.headers.common.Authorization;
|
||||||
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
DownloadLink,
|
DownloadLink,
|
||||||
DocumentResponse,
|
DocumentResponse,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface AssetResponse {
|
|||||||
mime_type: string;
|
mime_type: string;
|
||||||
metadata: Record<string, unknown>;
|
metadata: Record<string, unknown>;
|
||||||
download?: DownloadLink | null;
|
download?: DownloadLink | null;
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentVersionResponse {
|
export interface DocumentVersionResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user