mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 07:46:47 +00:00
Merge 1207a6b565 into 6b5596ec36
This commit is contained in:
commit
7afa40d914
2 changed files with 101 additions and 5 deletions
|
|
@ -85,7 +85,7 @@ describe('axios 401 interceptor — Authorization header guard', () => {
|
|||
|
||||
mockAdapter.mockRejectedValueOnce({
|
||||
response: { status: 401 },
|
||||
config: { url: '/api/share/abc123', headers: {} },
|
||||
config: { url: '/api/share/abc123', method: 'get', headers: {} },
|
||||
});
|
||||
|
||||
mockAdapter.mockResolvedValueOnce({
|
||||
|
|
@ -114,6 +114,75 @@ describe('axios 401 interceptor — Authorization header guard', () => {
|
|||
expect(refreshCall[0].url).toContain('api/auth/refresh');
|
||||
});
|
||||
|
||||
it('recognizes base-prefixed shared link data requests', async () => {
|
||||
expect.assertions(2);
|
||||
setTokenHeader(undefined);
|
||||
|
||||
setWindowLocation({
|
||||
href: 'http://localhost/chat/share/abc123',
|
||||
pathname: '/chat/share/abc123',
|
||||
search: '',
|
||||
hash: '',
|
||||
origin: 'http://localhost',
|
||||
} as Partial<Location>);
|
||||
|
||||
mockAdapter.mockRejectedValueOnce({
|
||||
response: { status: 401 },
|
||||
config: { url: '/chat/api/share/abc123', method: 'get', headers: {} },
|
||||
});
|
||||
|
||||
mockAdapter.mockResolvedValueOnce({
|
||||
data: { token: 'new-token' },
|
||||
status: 200,
|
||||
headers: {},
|
||||
config: {},
|
||||
});
|
||||
|
||||
mockAdapter.mockResolvedValueOnce({
|
||||
data: { sharedLink: {} },
|
||||
status: 200,
|
||||
headers: {},
|
||||
config: {},
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.get('/chat/api/share/abc123');
|
||||
} catch {
|
||||
// may reject depending on exact flow
|
||||
}
|
||||
|
||||
expect(mockAdapter.mock.calls.length).toBe(3);
|
||||
|
||||
const refreshCall = mockAdapter.mock.calls[1];
|
||||
expect(refreshCall[0].url).toContain('api/auth/refresh');
|
||||
});
|
||||
|
||||
it('does not refresh or redirect for unrelated 401s on public shared link pages', async () => {
|
||||
expect.assertions(2);
|
||||
setTokenHeader(undefined);
|
||||
|
||||
setWindowLocation({
|
||||
href: 'http://localhost/share/abc123',
|
||||
pathname: '/share/abc123',
|
||||
search: '',
|
||||
hash: '',
|
||||
} as Partial<Location>);
|
||||
|
||||
mockAdapter.mockRejectedValueOnce({
|
||||
response: { status: 401 },
|
||||
config: { url: '/api/mcp/servers', headers: {} },
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.get('/api/mcp/servers');
|
||||
} catch {
|
||||
// expected rejection
|
||||
}
|
||||
|
||||
expect(mockAdapter).toHaveBeenCalledTimes(1);
|
||||
expect(window.location.href).toBe('http://localhost/share/abc123');
|
||||
});
|
||||
|
||||
it('does not bypass guard when share/ appears only in query params', async () => {
|
||||
expect.assertions(1);
|
||||
setTokenHeader(undefined);
|
||||
|
|
@ -152,7 +221,7 @@ describe('axios 401 interceptor — Authorization header guard', () => {
|
|||
|
||||
mockAdapter.mockRejectedValueOnce({
|
||||
response: { status: 401 },
|
||||
config: { url: '/api/share/abc123', headers: {} },
|
||||
config: { url: '/api/share/abc123', method: 'get', headers: {} },
|
||||
});
|
||||
|
||||
mockAdapter.mockResolvedValueOnce({
|
||||
|
|
@ -184,7 +253,7 @@ describe('axios 401 interceptor — Authorization header guard', () => {
|
|||
|
||||
mockAdapter.mockRejectedValueOnce({
|
||||
response: { status: 401 },
|
||||
config: { url: '/api/share/abc123', headers: {} },
|
||||
config: { url: '/api/share/abc123', method: 'get', headers: {} },
|
||||
});
|
||||
|
||||
mockAdapter.mockResolvedValueOnce({
|
||||
|
|
|
|||
|
|
@ -67,6 +67,32 @@ let failedQueue: { resolve: (value?: any) => void; reject: (reason?: any) => voi
|
|||
const refreshToken = (retry?: boolean): Promise<t.TRefreshTokenResponse | undefined> =>
|
||||
_post(endpoints.refreshToken(retry));
|
||||
|
||||
const stripBasePath = (pathname: string) => {
|
||||
const baseUrl = endpoints.apiBaseUrl();
|
||||
if (baseUrl && (pathname === baseUrl || pathname.startsWith(`${baseUrl}/`))) {
|
||||
return pathname.slice(baseUrl.length) || '/';
|
||||
}
|
||||
return pathname;
|
||||
};
|
||||
|
||||
const isSharePage = () =>
|
||||
/(?:^|\/)share\/[^/]+\/?$/.test(stripBasePath(window.location.pathname));
|
||||
|
||||
const getRequestPathname = (url?: string) => {
|
||||
if (typeof url !== 'string') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new URL(url, window.location.origin).pathname;
|
||||
} catch {
|
||||
return url.split(/[?#]/)[0] ?? '';
|
||||
}
|
||||
};
|
||||
|
||||
const isSharedMessagesRequest = (url?: string, method?: string) =>
|
||||
method?.toLowerCase() === 'get' &&
|
||||
/(?:^|\/)api\/share\/[^/]+$/.test(getRequestPathname(url));
|
||||
|
||||
const dispatchTokenUpdatedEvent = (token: string) => {
|
||||
setTokenHeader(token);
|
||||
window.dispatchEvent(new CustomEvent('tokenUpdated', { detail: token }));
|
||||
|
|
@ -100,10 +126,11 @@ if (typeof window !== 'undefined') {
|
|||
}
|
||||
|
||||
/** Skip refresh when the Authorization header has been cleared (e.g. during logout),
|
||||
* but allow shared link requests to proceed so auth recovery/redirect can happen */
|
||||
* but allow the shared link data request to proceed so private shares can still
|
||||
* recover auth/redirect without unrelated share-page queries forcing login. */
|
||||
if (
|
||||
!axios.defaults.headers.common['Authorization'] &&
|
||||
!window.location.pathname.startsWith('/share/')
|
||||
!(isSharePage() && isSharedMessagesRequest(originalRequest.url, originalRequest.method))
|
||||
) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue