Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code ee2f8e0226 fix: handle missing protocol in origin for getSubdomainAndDomainFromUrl
https://sonarly.com/issue/4546?type=bug

The `GetPublicWorkspaceDataByDomain` query crashes with `TypeError: Invalid URL` when the `origin` variable is a bare hostname without a protocol scheme (e.g., `hslmx.twenty.com` instead of `https://hslmx.twenty.com`).

Fix: Added URL protocol normalization in `DomainServerConfigService.getSubdomainAndDomainFromUrl()`. When the input URL string lacks a protocol scheme (e.g., `hslmx.twenty.com` instead of `https://hslmx.twenty.com`), the method now prepends `https://` before passing it to `new URL()`. This prevents the `TypeError: Invalid URL` that occurs when API clients call `GetPublicWorkspaceDataByDomain` with a bare hostname.

The check `url.includes('://')` is used rather than a regex to stay consistent with the codebase's simple string operations. Full URLs (from browser `window.location.origin`) pass through unchanged.

Added 3 test cases to the existing test file:
1. Extracts subdomain from a full URL with protocol (existing behavior preserved)
2. Handles URL without protocol scheme (the bug fix)
3. Returns custom domain for non-front domain URLs
2026-03-27 20:21:58 +00:00
2 changed files with 71 additions and 1 deletions
@@ -42,6 +42,74 @@ describe('SubdomainManagerService', () => {
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
});
describe('getSubdomainAndDomainFromUrl', () => {
it('should extract subdomain from a full URL with protocol', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://twenty.com',
DEFAULT_SUBDOMAIN: 'app',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result =
domainServerConfigService.getSubdomainAndDomainFromUrl(
'https://myworkspace.twenty.com',
);
expect(result.subdomain).toBe('myworkspace');
expect(result.domain).toBeNull();
});
it('should handle URL without protocol scheme', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://twenty.com',
DEFAULT_SUBDOMAIN: 'app',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result =
domainServerConfigService.getSubdomainAndDomainFromUrl(
'myworkspace.twenty.com',
);
expect(result.subdomain).toBe('myworkspace');
expect(result.domain).toBeNull();
});
it('should return custom domain for non-front domain URLs', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://twenty.com',
DEFAULT_SUBDOMAIN: 'app',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result =
domainServerConfigService.getSubdomainAndDomainFromUrl(
'custom.example.com',
);
expect(result.subdomain).toBeUndefined();
expect(result.domain).toBe('custom.example.com');
});
});
describe('buildBaseUrl', () => {
it('should build the base URL from environment variables', () => {
jest
@@ -46,7 +46,9 @@ export class DomainServerConfigService {
}
getSubdomainAndDomainFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
const urlWithProtocol = url.includes('://') ? url : `https://${url}`;
const { hostname: originHostname } = new URL(urlWithProtocol);
const frontDomain = this.getFrontUrl().hostname;