diff --git a/lib/core/src/lib/auth/services/bff/bff-auth.service.spec.ts b/lib/core/src/lib/auth/services/bff/bff-auth.service.spec.ts index 4ff2687543..10e753fc29 100644 --- a/lib/core/src/lib/auth/services/bff/bff-auth.service.spec.ts +++ b/lib/core/src/lib/auth/services/bff/bff-auth.service.spec.ts @@ -15,15 +15,19 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; +import { HttpHeaders, provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { BffAuthService, BffUserResponse } from './bff-auth.service'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { firstValueFrom } from 'rxjs'; -import { provideHttpClient, HttpHeaders } from '@angular/common/http'; +import { BffAuthService, BffUserResponse } from './bff-auth.service'; +import { BffUrlBuilder } from './bff-url-builder.service'; +import { DOCUMENT } from '@angular/common'; describe('BffAuthService', () => { let service: BffAuthService; let httpMock: HttpTestingController; + let urlBuilder: jasmine.SpyObj; + let mockDocument: any; const mockAuthenticatedUser: BffUserResponse = { isAuthenticated: true, @@ -57,11 +61,40 @@ describe('BffAuthService', () => { } }; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [BffAuthService, provideHttpClient(), provideHttpClientTesting()] - }); + // eslint-disable-next-line jsdoc/require-jsdoc + function flushConstructorRequests() { + httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockAuthenticatedUser)); + } + beforeEach(() => { + urlBuilder = jasmine.createSpyObj('BffUrlBuilder', ['getUserUrl', 'getLoginUrl', 'getLogoutUrl']); + urlBuilder.getUserUrl.and.returnValue('http://hawkins-lab:1983/fakePath/bff/user'); + urlBuilder.getLoginUrl.and.callFake((returnUrl?: string) => { + if (!returnUrl || returnUrl === '/') { + return 'http://hawkins-lab:1983/fakePath/bff/login'; + } + return `http://hawkins-lab:1983/fakePath/bff/login?returnUrl=${encodeURIComponent(returnUrl)}`; + }); + urlBuilder.getLogoutUrl.and.returnValue('http://hawkins-lab:1983/fakePath/bff/logout'); + + mockDocument = { + location: { + href: '', + reload: () => {} + } + }; + + TestBed.configureTestingModule({ + providers: [ + BffAuthService, + { provide: BffUrlBuilder, useValue: urlBuilder }, + { provide: DOCUMENT, useValue: mockDocument }, + provideHttpClient(), + provideHttpClientTesting() + ] + }); + spyOn(mockDocument.location, 'reload'); + service = TestBed.inject(BffAuthService); httpMock = TestBed.inject(HttpTestingController); }); @@ -70,55 +103,32 @@ describe('BffAuthService', () => { }); describe('getUser', () => { - it('should return authenticated user when user is logged in', async () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should call urlBuilder.getUserUrl and return authenticated user', async () => { + flushConstructorRequests(); const resultPromise = firstValueFrom(service.getUser()); - - const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user')); + const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue); expect(req.request.method).toBe('GET'); req.flush(mockAuthenticatedUser); - const result = await resultPromise; - + expect(urlBuilder.getUserUrl).toHaveBeenCalled(); expect(result).toEqual(mockAuthenticatedUser); - expect(result.isAuthenticated).toBe(true); - expect(result.user.email).toBe('test@example.com'); }); it('should return unauthenticated user when user is not logged in', async () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser)); - + httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockUnauthenticatedUser)); const resultPromise = firstValueFrom(service.getUser()); - - const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user')); + const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue); expect(req.request.method).toBe('GET'); req.flush(mockUnauthenticatedUser); - const result = await resultPromise; - expect(result).toEqual(mockUnauthenticatedUser); - expect(result.isAuthenticated).toBe(false); }); it('should handle error when getUser fails', async () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' })); - + flushConstructorRequests(); try { const result = firstValueFrom(service.getUser()); - const req = httpMock.expectOne((req) => req.url.includes('/bff/user')); + const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue); req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' }); await result; fail('Should have thrown an error'); @@ -127,163 +137,94 @@ describe('BffAuthService', () => { } }); - it('should use correct URL format with protocol and host', async () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should use custom URL from urlBuilder', async () => { + flushConstructorRequests(); service.getUser().subscribe(); - - const req = httpMock.expectOne((req) => req.url.includes('/bff/user')); - expect(req.request.url).toMatch(/^https?:\/\/.+\/bff\/user$/); - req.flush(mockAuthenticatedUser); + const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue); + expect(req.request.url).toBe('http://hawkins-lab:1983/fakePath/bff/user'); }); }); describe('login', () => { - it('should redirect to /bff/login when no returnUrl is provided', () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should redirect to login url when no returnUrl is provided', () => { + flushConstructorRequests(); service.login(); + expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith(undefined); + expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue); }); - - it('should redirect to /bff/login when returnUrl is root', () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should redirect to login url when returnUrl is root', () => { + flushConstructorRequests(); service.login('/'); + expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/'); + expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue); }); - - it('should redirect to /bff/login with returnUrl when currentUrl is provided', () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should redirect to login url with returnUrl when currentUrl is provided', () => { + flushConstructorRequests(); service.login('/dashboard'); + expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/dashboard'); + expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue); }); - it('should properly encode returnUrl parameter', () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + flushConstructorRequests(); service.login('/path?query=value&other=data'); + expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/path?query=value&other=data'); + expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue); }); }); describe('logout', () => { - it('should call /bff/logout and redirect to default location on success', async () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should call urlBuilder.getLogoutUrl and redirect to default location on success', () => { + flushConstructorRequests(); service.logout(); - - const req = httpMock.expectOne((req) => req.url.includes('/bff/logout')); + const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue); expect(req.request.method).toBe('POST'); - expect(req.request.body).toEqual({}); req.flush({}); + expect(mockDocument.location.href).toBe('/'); }); - - it('should redirect to custom redirectTo location when provided', async () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should redirect to custom redirectTo location when provided', () => { + flushConstructorRequests(); service.logout(); - - const req = httpMock.expectOne((req) => req.url.includes('/bff/logout')); + const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue); req.flush({ redirectTo: '/custom-logout' }); + expect(mockDocument.location.href).toBe('/custom-logout'); }); - - it('should reload page on logout error', async () => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - + it('should reload page on logout error', () => { + flushConstructorRequests(); service.logout(); - - const req = httpMock.expectOne((req) => req.url.includes('/bff/logout')); + const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue); req.flush('Server Error', { status: 500, statusText: 'Server Error' }); + expect(mockDocument.location.reload).toHaveBeenCalled(); }); }); describe('constructor', () => { - it('should set isAuthenticated to true when user is authenticated', (done) => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - - service.onLogin.subscribe((isLoggedIn) => { - if (isLoggedIn) { - expect(service.isAuthenticated).toBe(true); - done(); - } + it('should set isAuthenticated to true when user is authenticated', async () => { + flushConstructorRequests(); + const isLoggedIn = await firstValueFrom(service.onLogin); + expect(isLoggedIn).toBe(true); + expect(service.isAuthenticated).toBe(true); + }); + it('should not emit onLogin when user is not authenticated', fakeAsync(() => { + httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockUnauthenticatedUser)); + let emitted = false; + service.onLogin.subscribe((val) => { + if (val) emitted = true; }); - }); - - it('should not emit onLogin when user is not authenticated', (done) => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser)); - - let loginEmitted = false; - service.onLogin.subscribe((isLoggedIn) => { - if (isLoggedIn) { - loginEmitted = true; - } - }); - - setTimeout(() => { - expect(loginEmitted).toBe(false); - expect(service.isAuthenticated).toBe(false); - done(); - }, 100); - }); - - it('should populate userInfo from getUser response', (done) => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); - expect(bffUserRequestMatchers.length).toBe(2); - bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); - - setTimeout(() => { - expect(service.userInfo).toEqual(mockAuthenticatedUser); - expect(service.userInfo.user.email).toBe('test@example.com'); - done(); - }, 100); - }); + tick(100); + expect(emitted).toBe(false); + expect(service.isAuthenticated).toBe(false); + })); + it('should populate userInfo from getUser response', fakeAsync(() => { + flushConstructorRequests(); + tick(100); + expect(service.userInfo).toEqual(mockAuthenticatedUser); + expect(service.userInfo.user.email).toBe('test@example.com'); + })); }); describe('interface methods', () => { beforeEach(() => { - service = TestBed.inject(BffAuthService); - - const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user')); + const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/fakePath/bff/user')); expect(bffUserRequestMatchers.length).toBe(2); bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser)); }); diff --git a/lib/core/src/lib/auth/services/bff/bff-auth.service.ts b/lib/core/src/lib/auth/services/bff/bff-auth.service.ts index 09c051f458..9beaa9cc70 100644 --- a/lib/core/src/lib/auth/services/bff/bff-auth.service.ts +++ b/lib/core/src/lib/auth/services/bff/bff-auth.service.ts @@ -16,10 +16,12 @@ */ import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { EventEmitter } from 'eventemitter3'; import { BehaviorSubject, filter, Observable } from 'rxjs'; import { AuthenticationServiceInterface } from '../../interfaces/authentication-service.interface'; -import { EventEmitter } from 'eventemitter3'; +import { BffUrlBuilder } from './bff-url-builder.service'; export interface BffUserInfo { sub: string; @@ -57,7 +59,11 @@ export class BffAuthService implements AuthenticationServiceInterface { once: EventEmitterInstance['once']; emit: EventEmitterInstance['emit']; - constructor(private http: HttpClient) { + constructor( + private http: HttpClient, + private urlBuilder: BffUrlBuilder, + @Inject(DOCUMENT) private document: Document + ) { this.getUser() .pipe(filter((user) => user.isAuthenticated)) .subscribe(() => { @@ -65,7 +71,6 @@ export class BffAuthService implements AuthenticationServiceInterface { this.onLogin.next(this.isAuthenticated); }); this.getUser().subscribe((userResponse) => { - console.log('[BffAuthService] userResponse: ', userResponse); this.userInfo = userResponse; }); } @@ -102,36 +107,20 @@ export class BffAuthService implements AuthenticationServiceInterface { } getUser(): Observable { - const protocol = window.location.protocol; - const host = window.location.host; - console.log('[BffAuthService] getUser from ', `${protocol}//${host}/bff/user`); - return this.http.get(`${protocol}//${host}/bff/user`); + return this.http.get(this.urlBuilder.getUserUrl()); } login(currentUrl?: string): Promise | void { - const protocol = window.location.protocol; - const host = window.location.host; - let url: string; - if (!currentUrl || currentUrl === '/') { - url = `${protocol}//${host}/bff/login`; - } else { - url = `${protocol}//${host}/bff/login?returnUrl=${encodeURIComponent(currentUrl ?? '')}`; - } - - console.log('url: ', url); - window.location.href = url; + this.document.location.href = this.urlBuilder.getLoginUrl(currentUrl); } logout(): Promise | void { - const protocol = window.location.protocol; - const host = window.location.host; - this.http.post<{ redirectTo?: string }>(`${protocol}//${host}/bff/logout`, {}).subscribe({ + this.http.post<{ redirectTo?: string }>(this.urlBuilder.getLogoutUrl(), {}).subscribe({ next: (res) => { - console.log('[BffAuthService] logout: ', res); const target = res.redirectTo || '/'; - window.location.href = target; + this.document.location.href = target; }, - error: () => window.location.reload() + error: () => this.document.location.reload() }); } } diff --git a/lib/core/src/lib/auth/services/bff/bff-url-builder.service.spec.ts b/lib/core/src/lib/auth/services/bff/bff-url-builder.service.spec.ts new file mode 100644 index 0000000000..f72d4591c6 --- /dev/null +++ b/lib/core/src/lib/auth/services/bff/bff-url-builder.service.spec.ts @@ -0,0 +1,100 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DOCUMENT } from '@angular/common'; +import { TestBed } from '@angular/core/testing'; +import { BffUrlBuilder } from './bff-url-builder.service'; + +describe('BffUrlBuilder', () => { + let service: BffUrlBuilder; + let mockDocument: Document; + + // eslint-disable-next-line jsdoc/require-jsdoc + function setFakeDocumentLocation(pathname: string, protocol = 'https:', host = 'hawkins-lab:1983') { + mockDocument.location = { protocol, host, pathname } as any; + } + + beforeEach(() => { + mockDocument = { + location: { protocol: 'https:', host: 'hawkins-lab:1983', pathname: '/' } + } as any; + TestBed.configureTestingModule({ + providers: [BffUrlBuilder, { provide: DOCUMENT, useValue: mockDocument }] + }); + service = TestBed.inject(BffUrlBuilder); + }); + + describe('getUserUrl', () => { + it('should build user url with valid path segment', () => { + setFakeDocumentLocation('/app1'); + expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/app1/bff/user'); + }); + it('should build user url without path segment if invalid', () => { + setFakeDocumentLocation('/'); + expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user'); + }); + it('should build user url without path segment if segment is invalid', () => { + setFakeDocumentLocation('/123bad'); + expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user'); + }); + }); + + describe('getLoginUrl', () => { + it('should build login url with valid path segment and no returnUrl', () => { + setFakeDocumentLocation('/app1'); + expect(service.getLoginUrl()).toBe('https://hawkins-lab:1983/app1/bff/login'); + }); + it('should build login url without path segment if invalid and no returnUrl', () => { + setFakeDocumentLocation('/'); + expect(service.getLoginUrl()).toBe('https://hawkins-lab:1983/bff/login'); + }); + it('should build login url with valid path segment and returnUrl', () => { + setFakeDocumentLocation('/app1'); + expect(service.getLoginUrl('/dashboard')).toBe('https://hawkins-lab:1983/app1/bff/login?returnUrl=%2Fdashboard'); + }); + it('should build login url without path segment if invalid and with returnUrl', () => { + setFakeDocumentLocation('/'); + expect(service.getLoginUrl('/dashboard')).toBe('https://hawkins-lab:1983/bff/login?returnUrl=%2Fdashboard'); + }); + }); + + describe('getLogoutUrl', () => { + it('should build logout url with valid path segment', () => { + setFakeDocumentLocation('/app1'); + expect(service.getLogoutUrl()).toBe('https://hawkins-lab:1983/app1/bff/logout'); + }); + it('should build logout url without path segment if invalid', () => { + setFakeDocumentLocation('/'); + expect(service.getLogoutUrl()).toBe('https://hawkins-lab:1983/bff/logout'); + }); + }); + + describe('edge cases for path segment', () => { + it('should ignore path segment with special characters', () => { + setFakeDocumentLocation('/app$1'); + expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user'); + }); + it('should ignore path segment with spaces', () => { + setFakeDocumentLocation('/app 1'); + expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user'); + }); + it('should ignore empty path segment', () => { + setFakeDocumentLocation(''); + expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user'); + }); + }); +}); diff --git a/lib/core/src/lib/auth/services/bff/bff-url-builder.service.ts b/lib/core/src/lib/auth/services/bff/bff-url-builder.service.ts new file mode 100644 index 0000000000..60ddce1fff --- /dev/null +++ b/lib/core/src/lib/auth/services/bff/bff-url-builder.service.ts @@ -0,0 +1,90 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Inject, Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +@Injectable({ providedIn: 'root' }) +export class BffUrlBuilder { + constructor(@Inject(DOCUMENT) private document: Document) {} + + private getValidPathSegment(pathname: string): string { + if (!pathname || pathname === '/') return ''; + const segment = pathname.split('/')[1] || ''; + if (/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(segment)) { + return segment; + } + return ''; + } + + private getProtocolHost(): { protocol: string; host: string; pathname: string } { + const { protocol, host, pathname } = this.document.location; + return { protocol, host, pathname }; + } + + /** + * Constructs and returns the URL for the BFF user endpoint. + * + * The URL is built using the protocol, host, and pathname obtained from `getProtocolHost()`. + * If a valid path segment is present (determined by `getValidPathSegment(pathname)`), it is included in the URL path. + * Otherwise, the URL is constructed without the path segment. + * + * @returns The fully constructed BFF user endpoint URL. + */ + getUserUrl(): string { + const { protocol, host, pathname } = this.getProtocolHost(); + const pathSegment = this.getValidPathSegment(pathname); + return pathSegment ? `${protocol}//${host}/${pathSegment}/bff/user` : `${protocol}//${host}/bff/user`; + } + + /** + * Constructs the login URL for the BFF (Backend For Frontend) authentication service. + * + * If `currentUrl` is not provided or is `'/'`, returns the login URL without a `returnUrl` query parameter. + * Otherwise, appends the encoded `currentUrl` as the `returnUrl` query parameter to the login URL. + * + * The URL is built using the protocol, host, and an optional path segment derived from the current location. + * + * @param currentUrl - The URL to redirect to after successful login. If omitted or `'/'`, no redirect is specified. + * @returns The constructed login URL as a string. + */ + getLoginUrl(currentUrl?: string): string { + const { protocol, host, pathname } = this.getProtocolHost(); + const pathSegment = this.getValidPathSegment(pathname); + if (!currentUrl || currentUrl === '/') { + return pathSegment ? `${protocol}//${host}/${pathSegment}/bff/login` : `${protocol}//${host}/bff/login`; + } + const returnUrl = encodeURIComponent(currentUrl ?? ''); + return pathSegment + ? `${protocol}//${host}/${pathSegment}/bff/login?returnUrl=${returnUrl}` + : `${protocol}//${host}/bff/login?returnUrl=${returnUrl}`; + } + + /** + * Constructs and returns the logout URL for the BFF (Backend For Frontend) service. + * The URL is built using the protocol, host, and an optional valid path segment + * obtained from the current location. If a valid path segment exists, it is included + * in the URL path before `/bff/logout`; otherwise, the URL defaults to `/bff/logout`. + * + * @returns The fully qualified logout URL for the BFF service. + */ + getLogoutUrl(): string { + const { protocol, host, pathname } = this.getProtocolHost(); + const pathSegment = this.getValidPathSegment(pathname); + return pathSegment ? `${protocol}//${host}/${pathSegment}/bff/logout` : `${protocol}//${host}/bff/logout`; + } +}