Sprint 9: add tenant-driven API resolution layer

This commit is contained in:
sdarbinyan
2026-07-05 02:24:16 +04:00
parent c3d1153f0e
commit 487a3fb913
12 changed files with 197 additions and 17 deletions

View File

@@ -0,0 +1,86 @@
import { Injectable, inject } from '@angular/core';
import { environment } from '../../../environments/environment';
import { ConfigService } from './config.service';
import { TenantResolverService } from './tenant-resolver.service';
@Injectable({ providedIn: 'root' })
export class ApiConfigService {
private readonly tenantResolver = inject(TenantResolverService);
private readonly configService = inject(ConfigService);
getBaseUrl(): string {
const tenantKey = this.tenantResolver.getTenantKey();
const bootstrapUrl = this.resolveBootstrapApiBaseUrl();
const tenantMap = (environment as any).tenantApiBaseUrls as Record<string, string> | undefined;
const localhostUrl = (environment as any).localhostApiUrl as string | undefined;
const apiTemplate = (environment as any).tenantApiTemplate as string | undefined;
let url = environment.apiUrl;
if (this.tenantResolver.isLocalhost() && localhostUrl) {
url = localhostUrl;
} else if (bootstrapUrl) {
url = bootstrapUrl;
} else if (tenantMap?.[tenantKey]) {
url = tenantMap[tenantKey];
} else if (apiTemplate) {
url = apiTemplate.replace('{tenant}', tenantKey);
}
return this.normalizeBaseUrl(url);
}
isApiRequest(url: string): boolean {
if (!url) {
return false;
}
if (url.startsWith('/api')) {
return true;
}
const baseUrl = this.getBaseUrl();
return url.startsWith(baseUrl);
}
toApiUrl(url: string): string {
if (!url || /^https?:\/\//i.test(url)) {
return url;
}
if (!url.startsWith('/api')) {
return url;
}
const baseUrl = this.getBaseUrl();
const path = url.slice('/api'.length);
return `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
}
private resolveBootstrapApiBaseUrl(): string | null {
const bootstrap = this.configService.getBootstrapSnapshot() as any;
if (!bootstrap) {
return null;
}
const endpointBase = bootstrap?.apiEndpoints?.website?.baseUrl;
if (typeof endpointBase === 'string' && endpointBase.trim().length > 0) {
return endpointBase;
}
const tenantBase = bootstrap?.tenant?.apiBaseUrl;
if (typeof tenantBase === 'string' && tenantBase.trim().length > 0) {
return tenantBase;
}
return null;
}
private normalizeBaseUrl(url: string): string {
if (!url || url === '/') {
return '/';
}
return url.replace(/\/+$/, '');
}
}

View File

@@ -0,0 +1,36 @@
import { DOCUMENT } from '@angular/common';
import { Injectable, inject } from '@angular/core';
import { environment } from '../../../environments/environment';
@Injectable({ providedIn: 'root' })
export class TenantResolverService {
private readonly document = inject(DOCUMENT);
getHostname(): string {
const host = this.document?.location?.hostname ?? '';
return host.toLowerCase();
}
isLocalhost(): boolean {
const hostname = this.getHostname();
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
}
getTenantKey(): string {
if (this.isLocalhost()) {
return (environment as any).fallbackTenantKey ?? 'default';
}
const hostname = this.getHostname();
const segments = hostname.split('.').filter(Boolean);
if (segments.length === 0) {
return (environment as any).fallbackTenantKey ?? 'default';
}
if (segments[0] === 'www' && segments.length > 1) {
return segments[1];
}
return segments[0];
}
}