Merge branch 'refactor/tenant_matching' into fix/minor_fixes

This commit is contained in:
2026-08-18 17:24:09 -03:00
7 changed files with 159 additions and 2 deletions

View File

@@ -4,7 +4,7 @@ import {
provideAppInitializer,
provideBrowserGlobalErrorListeners,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, UrlSerializer } from '@angular/router';
import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';
import { routes } from './app.routes';
@@ -12,6 +12,7 @@ import { authBootstrap } from './core/services/auth/auth-bootstrap';
import { authInterceptor } from './core/services/auth/auth.interceptor';
import { globalLoadingInterceptor } from './core/services/global-loading/global-loading.interceptor';
import { tenantBootstrap } from './core/services/tenant-bootstrap';
import { TenantUrlSerializer } from './core/services/tenant-url.serializer';
export function isStoreCatalogRequest(request: HttpRequest<unknown>): boolean {
return (
@@ -26,6 +27,7 @@ export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
{ provide: UrlSerializer, useClass: TenantUrlSerializer },
provideClientHydration(
withHttpTransferCacheOptions({
includeRequestsWithAuthHeaders: true,

View File

@@ -14,5 +14,12 @@ export const routes: Routes = [
{
path: '',
loadChildren: () => import('./features/store/store.routes').then((m) => m.routes)
},
{
path: '**',
loadComponent: () =>
import('./shared/pages/route-not-found-page.component').then(
(m) => m.RouteNotFoundPageComponent,
)
}
];

View File

@@ -119,8 +119,12 @@ export class AuthService extends BaseApiService {
const apiUrl = new URL(environment.url);
const authorizationUrl = new URL('/auth/google/redirect', apiUrl.origin);
const basePath = tenant.base_path && tenant.base_path !== '/' ? tenant.base_path : '';
authorizationUrl.searchParams.set('tenant', tenant.codigo);
authorizationUrl.searchParams.set('return_url', this.document.location.origin);
authorizationUrl.searchParams.set(
'return_url',
`${this.document.location.origin}${basePath}`,
);
this.document.location.assign(authorizationUrl.toString());
}

View File

@@ -0,0 +1,50 @@
import '@angular/compiler';
import { DefaultUrlSerializer } from '@angular/router';
import { describe, expect, it } from 'vitest';
import { Tenant } from './tenant.interface';
import { TenantService } from './tenant.service';
import { TenantUrlSerializer } from './tenant-url.serializer';
describe('TenantUrlSerializer', () => {
const defaultSerializer = new DefaultUrlSerializer();
function createSerializer(basePath: string): TenantUrlSerializer {
const tenantService = {
getTenant: () => ({ base_path: basePath }) as Tenant,
} as TenantService;
return new TenantUrlSerializer(tenantService);
}
it('keeps root tenants unchanged', () => {
const serializer = createSerializer('/');
const tree = serializer.parse('/producto/123?ref=home');
expect(defaultSerializer.serialize(tree)).toBe('/producto/123?ref=home');
expect(serializer.serialize(tree)).toBe('/producto/123?ref=home');
});
it('removes the tenant base path when parsing and restores it when serializing', () => {
const serializer = createSerializer('/desfile');
const tree = serializer.parse('/desfile/producto/123?ref=home#detalle');
expect(defaultSerializer.serialize(tree)).toBe('/producto/123?ref=home#detalle');
expect(serializer.serialize(tree)).toBe('/desfile/producto/123?ref=home#detalle');
});
it('maps the tenant base path to the application root', () => {
const serializer = createSerializer('/desfile/');
const tree = serializer.parse('/desfile');
expect(defaultSerializer.serialize(tree)).toBe('/');
expect(serializer.serialize(tree)).toBe('/desfile');
});
it('does not strip partial path segment matches', () => {
const serializer = createSerializer('/desfile');
const tree = serializer.parse('/desfile-shop/producto/123');
expect(defaultSerializer.serialize(tree)).toBe('/desfile-shop/producto/123');
});
});

View File

@@ -0,0 +1,66 @@
import { Injectable } from '@angular/core';
import { DefaultUrlSerializer, UrlSerializer, UrlTree } from '@angular/router';
import { TenantService } from './tenant.service';
@Injectable()
export class TenantUrlSerializer extends UrlSerializer {
private readonly defaultSerializer = new DefaultUrlSerializer();
constructor(private readonly tenantService: TenantService) {
super();
}
override parse(url: string): UrlTree {
return this.defaultSerializer.parse(this.removeBasePath(url));
}
override serialize(tree: UrlTree): string {
const url = this.defaultSerializer.serialize(tree);
const basePath = this.basePath();
if (basePath === '/') {
return url;
}
return url === '/' ? basePath : `${basePath}${url}`;
}
private removeBasePath(url: string): string {
const basePath = this.basePath();
if (basePath === '/' || !this.startsWithCompletePathSegment(url, basePath)) {
return url;
}
const remainder = url.slice(basePath.length);
if (remainder === '') {
return '/';
}
return remainder.startsWith('?') || remainder.startsWith('#')
? `/${remainder}`
: remainder;
}
private basePath(): string {
const configuredPath = this.tenantService.getTenant()?.base_path?.trim() ?? '/';
if (configuredPath === '' || configuredPath === '/') {
return '/';
}
return `/${configuredPath.replace(/^\/+|\/+$/g, '')}`;
}
private startsWithCompletePathSegment(url: string, basePath: string): boolean {
if (!url.startsWith(basePath)) {
return false;
}
const boundary = url.charAt(basePath.length);
return boundary === '' || boundary === '/' || boundary === '?' || boundary === '#';
}
}

View File

@@ -101,6 +101,7 @@ export interface Tenant {
codigo: string;
nombre: string;
dominio: string;
base_path?: string;
site_title?: string | null;
favicon?: string | null;
primary_color: string;

View File

@@ -0,0 +1,27 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-route-not-found-page',
template: `
<main class="route-not-found">
<h1>404</h1>
<p>No encontramos la página solicitada.</p>
</main>
`,
styles: `
.route-not-found {
min-height: 100vh;
display: grid;
place-content: center;
gap: 0.5rem;
padding: 2rem;
text-align: center;
}
h1,
p {
margin: 0;
}
`,
})
export class RouteNotFoundPageComponent {}