Merge branch 'refactor/tenant_matching' into fix/minor_fixes
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
50
src/app/core/services/tenant-url.serializer.spec.ts
Normal file
50
src/app/core/services/tenant-url.serializer.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
66
src/app/core/services/tenant-url.serializer.ts
Normal file
66
src/app/core/services/tenant-url.serializer.ts
Normal 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 === '#';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
27
src/app/shared/pages/route-not-found-page.component.ts
Normal file
27
src/app/shared/pages/route-not-found-page.component.ts
Normal 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 {}
|
||||
Reference in New Issue
Block a user