🧭 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:
catmeme 2025-11-21 11:25:14 -05:00 committed by GitHub
parent ef3bf0a932
commit 7aa8d49f3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 717 additions and 30 deletions

View file

@ -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';

View 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');
});
});

View 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 '';
}
}

View file

@ -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');
});
});

View file

@ -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`;

View file

@ -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';

View file

@ -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);