fix(api): derive host from storefront domain
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Every storefront, including nested subdomains, uses its matching api.<hostname> endpoint.
This commit is contained in:
2026-08-20 14:31:22 +04:00
parent f4ea4c7af8
commit 640360d63c
15 changed files with 88 additions and 120 deletions

View File

@@ -16,7 +16,7 @@ describe('ApiBootstrapProvider', () => {
provideHttpClientTesting(),
{
provide: ApiConfigService,
useValue: { getBaseUrl: () => 'https://gorbushka.market/backend' }
useValue: { getBaseUrl: () => 'https://api.gorbushka.market' }
}
]
});
@@ -30,7 +30,7 @@ describe('ApiBootstrapProvider', () => {
it('loads bootstrap from the same tenant API base as every other request', () => {
provider.loadBootstrap().subscribe();
const request = httpTesting.expectOne('https://gorbushka.market/backend/bootstrap');
const request = httpTesting.expectOne('https://api.gorbushka.market/bootstrap');
expect(request.request.method).toBe('GET');
request.flush({});
});

View File

@@ -9,10 +9,10 @@ describe('ApiConfigService', () => {
beforeEach(() => {
tenantResolver = jasmine.createSpyObj<TenantResolverService>(
'TenantResolverService',
['getHostname', 'getOrigin', 'getTenantKey', 'isLocalhost']
['getHostname', 'getProtocol', 'getTenantKey', 'isLocalhost']
);
tenantResolver.getTenantKey.and.returnValue('gorbushka');
tenantResolver.getOrigin.and.returnValue('https://gorbushka.market');
tenantResolver.getProtocol.and.returnValue('https:');
tenantResolver.isLocalhost.and.returnValue(false);
TestBed.configureTestingModule({
@@ -28,26 +28,30 @@ describe('ApiConfigService', () => {
it('uses the current customer hostname for the production API base URL', () => {
tenantResolver.getHostname.and.returnValue('gorbushka.market');
expect(service.getBaseUrl()).toBe('https://gorbushka.market/backend');
expect(service.getBaseUrl()).toBe('https://api.gorbushka.market');
});
it('keeps www traffic on the same browser origin', () => {
tenantResolver.getHostname.and.returnValue('www.gorbushka.market');
tenantResolver.getOrigin.and.returnValue('https://www.gorbushka.market');
it('preserves every storefront subdomain in the API hostname', () => {
tenantResolver.getHostname.and.returnValue('store1.example.com');
expect(service.getBaseUrl()).toBe('https://www.gorbushka.market/backend');
expect(service.getBaseUrl()).toBe('https://api.store1.example.com');
});
it('preserves www like any other storefront subdomain', () => {
tenantResolver.getHostname.and.returnValue('www.gorbushka.market');
expect(service.getBaseUrl()).toBe('https://api.www.gorbushka.market');
});
it('preserves the API namespace when targeting a tenant backend', () => {
tenantResolver.getHostname.and.returnValue('gorbushka.market');
expect(service.toApiUrl('/api/v2/storefront/cart'))
.toBe('https://gorbushka.market/backend/api/v2/storefront/cart');
.toBe('https://api.gorbushka.market/api/v2/storefront/cart');
});
it('does not duplicate the API prefix for localhost proxy requests', () => {
tenantResolver.getHostname.and.returnValue('localhost');
tenantResolver.getOrigin.and.returnValue('http://localhost:4200');
tenantResolver.getTenantKey.and.returnValue('default');
tenantResolver.isLocalhost.and.returnValue(true);

View File

@@ -8,8 +8,7 @@ export class ApiConfigService {
getBaseUrl(): string {
const hostname = this.tenantResolver.getHostname();
const apiHostname = hostname.startsWith('www.') ? hostname.slice(4) : hostname;
const origin = this.tenantResolver.getOrigin();
const protocol = this.tenantResolver.getProtocol();
const tenantKey = this.tenantResolver.getTenantKey();
const tenantMap = (environment as any).tenantApiBaseUrls as Record<string, string> | undefined;
const localhostUrl = (environment as any).localhostApiUrl as string | undefined;
@@ -19,14 +18,14 @@ export class ApiConfigService {
if (this.tenantResolver.isLocalhost() && localhostUrl) {
url = localhostUrl;
} else if (tenantMap?.[hostname] || tenantMap?.[apiHostname]) {
url = tenantMap[hostname] ?? tenantMap[apiHostname];
} else if (tenantMap?.[hostname]) {
url = tenantMap[hostname];
} else if (tenantMap?.[tenantKey]) {
url = tenantMap[tenantKey];
} else if (apiTemplate && origin) {
} else if (apiTemplate && hostname) {
url = apiTemplate
.replace('{origin}', origin)
.replace('{hostname}', apiHostname)
.replace('{protocol}', protocol)
.replace('{hostname}', hostname)
.replace('{tenant}', tenantKey);
}

View File

@@ -11,8 +11,8 @@ export class TenantResolverService {
return host.toLowerCase();
}
getOrigin(): string {
return this.document?.location?.origin ?? '';
getProtocol(): string {
return this.document?.location?.protocol ?? 'https:';
}
isLocalhost(): boolean {

View File

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

View File

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