mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🧭 fix: Add Base Path Support for Login/Register and Image Paths (#10116)
* fix: add basePath pattern to support login/register and image paths * Fix linter errors * refactor: Update import statements for getBasePath and isEnabled, and add path utility functions with tests - Refactored imports in addImages.js and StableDiffusion.js to use getBasePath from '@librechat/api'. - Consolidated isEnabled and getBasePath imports in validateImageRequest.js. - Introduced new path utility functions in path.ts and corresponding unit tests in path.spec.ts to validate base path extraction logic. * fix: Update domain server base URL in MarkdownComponents and refactor authentication redirection logic - Changed the domain server base URL in MarkdownComponents.tsx to use the API base URL. - Refactored the useAuthRedirect hook to utilize React Router's navigate for redirection instead of window.location, ensuring a smoother SPA experience. - Added unit tests for the useAuthRedirect hook to verify authentication redirection behavior. * test: Mock isEnabled in validateImages.spec.js for improved test isolation - Updated validateImages.spec.js to mock the isEnabled function from @librechat/api, ensuring that tests can run independently of the actual implementation. - Cleared the DOMAIN_CLIENT environment variable before tests to avoid interference with basePath resolution. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
ef3bf0a932
commit
7aa8d49f3a
21 changed files with 717 additions and 30 deletions
|
|
@ -7,6 +7,7 @@ export * from './env';
|
|||
export * from './events';
|
||||
export * from './files';
|
||||
export * from './generators';
|
||||
export * from './path';
|
||||
export * from './key';
|
||||
export * from './latex';
|
||||
export * from './llm';
|
||||
|
|
|
|||
97
packages/api/src/utils/path.spec.ts
Normal file
97
packages/api/src/utils/path.spec.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import type { Logger } from '@librechat/agents';
|
||||
import { getBasePath } from './path';
|
||||
|
||||
describe('getBasePath', () => {
|
||||
let originalDomainClient: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalDomainClient = process.env.DOMAIN_CLIENT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.DOMAIN_CLIENT = originalDomainClient;
|
||||
});
|
||||
|
||||
it('should return empty string when DOMAIN_CLIENT is not set', () => {
|
||||
delete process.env.DOMAIN_CLIENT;
|
||||
expect(getBasePath()).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when DOMAIN_CLIENT is root path', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/';
|
||||
expect(getBasePath()).toBe('');
|
||||
});
|
||||
|
||||
it('should return base path for subdirectory deployment', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat';
|
||||
expect(getBasePath()).toBe('/librechat');
|
||||
});
|
||||
|
||||
it('should return base path without trailing slash', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat/';
|
||||
expect(getBasePath()).toBe('/librechat');
|
||||
});
|
||||
|
||||
it('should handle nested subdirectories', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/apps/librechat';
|
||||
expect(getBasePath()).toBe('/apps/librechat');
|
||||
});
|
||||
|
||||
it('should handle HTTPS URLs', () => {
|
||||
process.env.DOMAIN_CLIENT = 'https://example.com/librechat';
|
||||
expect(getBasePath()).toBe('/librechat');
|
||||
});
|
||||
|
||||
it('should handle URLs with query parameters', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat?param=value';
|
||||
expect(getBasePath()).toBe('/librechat');
|
||||
});
|
||||
|
||||
it('should handle URLs with fragments', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:3080/librechat#section';
|
||||
expect(getBasePath()).toBe('/librechat');
|
||||
});
|
||||
|
||||
it('should return empty string for invalid URL', () => {
|
||||
process.env.DOMAIN_CLIENT = 'not-a-valid-url';
|
||||
// Accepts (infoObject: object), return value is not used
|
||||
const loggerSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {
|
||||
return logger as unknown as Logger;
|
||||
});
|
||||
expect(getBasePath()).toBe('');
|
||||
expect(loggerSpy).toHaveBeenCalledWith(
|
||||
'Error parsing DOMAIN_CLIENT for base path:',
|
||||
expect.objectContaining({
|
||||
message: 'Invalid URL',
|
||||
}),
|
||||
);
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle empty string DOMAIN_CLIENT', () => {
|
||||
process.env.DOMAIN_CLIENT = '';
|
||||
expect(getBasePath()).toBe('');
|
||||
});
|
||||
|
||||
it('should handle undefined DOMAIN_CLIENT', () => {
|
||||
process.env.DOMAIN_CLIENT = undefined;
|
||||
expect(getBasePath()).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null DOMAIN_CLIENT', () => {
|
||||
// @ts-expect-error Testing null case
|
||||
process.env.DOMAIN_CLIENT = null;
|
||||
expect(getBasePath()).toBe('');
|
||||
});
|
||||
|
||||
it('should handle URLs with ports', () => {
|
||||
process.env.DOMAIN_CLIENT = 'http://localhost:8080/librechat';
|
||||
expect(getBasePath()).toBe('/librechat');
|
||||
});
|
||||
|
||||
it('should handle URLs with subdomains', () => {
|
||||
process.env.DOMAIN_CLIENT = 'https://app.example.com/librechat';
|
||||
expect(getBasePath()).toBe('/librechat');
|
||||
});
|
||||
});
|
||||
25
packages/api/src/utils/path.ts
Normal file
25
packages/api/src/utils/path.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
|
||||
/**
|
||||
* Gets the base path from the DOMAIN_CLIENT environment variable.
|
||||
* This is useful for constructing URLs when LibreChat is served from a subdirectory.
|
||||
* @returns {string} The base path (e.g., '/librechat' or '')
|
||||
*/
|
||||
export function getBasePath(): string {
|
||||
if (!process.env.DOMAIN_CLIENT) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const clientUrl = new URL(process.env.DOMAIN_CLIENT);
|
||||
// Keep consistent with the logic in api/server/index.js
|
||||
const baseHref = clientUrl.pathname.endsWith('/')
|
||||
? clientUrl.pathname.slice(0, -1) // Remove trailing slash for path construction
|
||||
: clientUrl.pathname;
|
||||
|
||||
return baseHref === '/' ? '' : baseHref;
|
||||
} catch (error) {
|
||||
logger.warn('Error parsing DOMAIN_CLIENT for base path:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
@ -60,8 +60,7 @@ describe('sanitizeTitle', () => {
|
|||
});
|
||||
|
||||
it('should handle multiple attributes', () => {
|
||||
const input =
|
||||
'<think reason="test" type="deep" id="1">reasoning</think> Title';
|
||||
const input = '<think reason="test" type="deep" id="1">reasoning</think> Title';
|
||||
expect(sanitizeTitle(input)).toBe('Title');
|
||||
});
|
||||
|
||||
|
|
@ -170,8 +169,7 @@ describe('sanitizeTitle', () => {
|
|||
});
|
||||
|
||||
it('should handle real-world with attributes', () => {
|
||||
const input =
|
||||
'<think reasoning="multi-step">\nStep 1\nStep 2\n</think> Project Status';
|
||||
const input = '<think reasoning="multi-step">\nStep 1\nStep 2\n</think> Project Status';
|
||||
expect(sanitizeTitle(input)).toBe('Project Status');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ export const resetPassword = () => `${BASE_URL}/api/auth/resetPassword`;
|
|||
|
||||
export const verifyEmail = () => `${BASE_URL}/api/user/verify`;
|
||||
|
||||
// Auth page URLs (for client-side navigation and redirects)
|
||||
export const loginPage = () => `${BASE_URL}/login`;
|
||||
export const registerPage = () => `${BASE_URL}/register`;
|
||||
|
||||
export const resendVerificationEmail = () => `${BASE_URL}/api/user/verify/resend`;
|
||||
|
||||
export const plugins = () => `${BASE_URL}/api/plugins`;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export * from './accessPermissions';
|
|||
export * from './keys';
|
||||
/* api call helpers */
|
||||
export * from './headers-helpers';
|
||||
export { loginPage, registerPage, apiBaseUrl } from './api-endpoints';
|
||||
export { default as request } from './request';
|
||||
export { dataService };
|
||||
import * as dataService from './data-service';
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ if (typeof window !== 'undefined') {
|
|||
`Refresh token failed from shared link, attempting request to ${originalRequest.url}`,
|
||||
);
|
||||
} else {
|
||||
window.location.href = '/login';
|
||||
window.location.href = endpoints.loginPage();
|
||||
}
|
||||
} catch (err) {
|
||||
processQueue(err as AxiosError, null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue