fix(api): share base-domain API host
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Tenant subdomains route through api.<base-domain>; nginx forwards the exact storefront host derived from the validated browser origin.
This commit is contained in:
2026-08-20 16:12:37 +04:00
parent e5949c3967
commit 9cd56586fb
15 changed files with 175 additions and 65 deletions

View File

@@ -9,8 +9,9 @@ describe('ApiConfigService', () => {
beforeEach(() => {
tenantResolver = jasmine.createSpyObj<TenantResolverService>(
'TenantResolverService',
['getHostname', 'getProtocol', 'getTenantKey', 'isLocalhost']
['getHostname', 'getBaseDomain', 'getProtocol', 'getTenantKey', 'isLocalhost']
);
tenantResolver.getBaseDomain.and.returnValue('gorbushka.market');
tenantResolver.getTenantKey.and.returnValue('gorbushka');
tenantResolver.getProtocol.and.returnValue('https:');
tenantResolver.isLocalhost.and.returnValue(false);
@@ -31,16 +32,17 @@ describe('ApiConfigService', () => {
expect(service.getBaseUrl()).toBe('https://api.gorbushka.market');
});
it('preserves every storefront subdomain in the API hostname', () => {
it('uses the shared base-domain API for a tenant subdomain', () => {
tenantResolver.getHostname.and.returnValue('store1.example.com');
tenantResolver.getBaseDomain.and.returnValue('example.com');
expect(service.getBaseUrl()).toBe('https://api.store1.example.com');
expect(service.getBaseUrl()).toBe('https://api.example.com');
});
it('preserves www like any other storefront subdomain', () => {
it('uses the shared base-domain API for www', () => {
tenantResolver.getHostname.and.returnValue('www.gorbushka.market');
expect(service.getBaseUrl()).toBe('https://api.www.gorbushka.market');
expect(service.getBaseUrl()).toBe('https://api.gorbushka.market');
});
it('preserves the API namespace when targeting a tenant backend', () => {

View File

@@ -8,6 +8,7 @@ export class ApiConfigService {
getBaseUrl(): string {
const hostname = this.tenantResolver.getHostname();
const baseDomain = this.tenantResolver.getBaseDomain();
const protocol = this.tenantResolver.getProtocol();
const tenantKey = this.tenantResolver.getTenantKey();
const tenantMap = (environment as any).tenantApiBaseUrls as Record<string, string> | undefined;
@@ -25,6 +26,7 @@ export class ApiConfigService {
} else if (apiTemplate && hostname) {
url = apiTemplate
.replace('{protocol}', protocol)
.replace('{baseDomain}', baseDomain)
.replace('{hostname}', hostname)
.replace('{tenant}', tenantKey);
}

View File

@@ -0,0 +1,33 @@
import { DOCUMENT } from '@angular/common';
import { TestBed } from '@angular/core/testing';
import { TenantResolverService } from './tenant-resolver.service';
describe('TenantResolverService', () => {
function resolveBaseDomain(hostname: string): string {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
providers: [
TenantResolverService,
{
provide: DOCUMENT,
useValue: { location: { hostname, protocol: 'https:' } }
}
]
});
return TestBed.inject(TenantResolverService).getBaseDomain();
}
it('keeps a root storefront domain', () => {
expect(resolveBaseDomain('example.com')).toBe('example.com');
});
it('removes tenant and www subdomains from the API domain', () => {
expect(resolveBaseDomain('store1.example.com')).toBe('example.com');
expect(resolveBaseDomain('www.example.com')).toBe('example.com');
});
it('keeps a country-code second-level domain', () => {
expect(resolveBaseDomain('store1.example.co.uk')).toBe('example.co.uk');
});
});

View File

@@ -15,6 +15,20 @@ export class TenantResolverService {
return this.document?.location?.protocol ?? 'https:';
}
getBaseDomain(): string {
const segments = this.getHostname().split('.').filter(Boolean);
if (segments.length <= 2) {
return segments.join('.');
}
const topLevelDomain = segments.at(-1) ?? '';
const secondLevelDomain = segments.at(-2) ?? '';
const usesCountryCodeSecondLevel =
topLevelDomain.length === 2 && secondLevelDomain.length <= 3;
return segments.slice(usesCountryCodeSecondLevel ? -3 : -2).join('.');
}
isLocalhost(): boolean {
const hostname = this.getHostname();
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';

View File

@@ -4,7 +4,7 @@ export const environment = {
useMockBootstrapOnLocal: false,
fallbackTenantKey: 'default',
localhostApiUrl: '/api',
tenantApiTemplate: '{protocol}//api.{hostname}',
tenantApiTemplate: '{protocol}//api.{baseDomain}',
tenantApiBaseUrls: {},
brandName: 'Marketplace',
brandFullName: 'Marketplace',

View File

@@ -5,7 +5,7 @@ export const environment = {
useMockBootstrapOnLocal: true,
fallbackTenantKey: 'default',
localhostApiUrl: '/api',
tenantApiTemplate: '{protocol}//api.{hostname}',
tenantApiTemplate: '{protocol}//api.{baseDomain}',
tenantApiBaseUrls: {},
brandName: 'Marketplace',
brandFullName: 'Marketplace',