fix(api): share base-domain API host
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
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:
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
33
src/app/core/config/tenant-resolver.service.spec.ts
Normal file
33
src/app/core/config/tenant-resolver.service.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user