diff --git a/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts b/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts index fe35ccebc77..6df4b577f2c 100644 --- a/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts +++ b/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts @@ -158,6 +158,82 @@ describe('LogInPasswordComponent', () => { }); }); + // Standalone login reads the redirect target from the `redirectUrl` query param (set by aai.js). + describe('standalone login redirect (redirectUrl query param)', () => { + let authService: AuthServiceStub; + let setRedirectUrlSpy: jasmine.Spy; + let setRedirectUrlIfNotSetSpy: jasmine.Spy; + + const setQueryParams = (queryParams: Record) => { + (component as any).route = { snapshot: { queryParams } }; + }; + + beforeEach(() => { + authService = TestBed.inject(AuthService) as unknown as AuthServiceStub; + setRedirectUrlSpy = spyOn(authService, 'setRedirectUrl').and.callThrough(); + setRedirectUrlIfNotSetSpy = spyOn(authService, 'setRedirectUrlIfNotSet').and.callThrough(); + // Avoid scheduling the real DiscoJuice popup timer during ngOnInit. + spyOn(component as any, 'popUpDiscoJuiceLogin'); + + fixture.detectChanges(); + component.form.controls.email.setValue('user'); + component.form.controls.password.setValue('password'); + }); + + it('redirects back to the redirectUrl page, reduced to an app-relative path', () => { + setQueryParams({ redirectUrl: 'http://dev-6.pc:8603/repository/search' }); + + component.submit(); + + expect(setRedirectUrlSpy).toHaveBeenCalledWith('/repository/search'); + expect(setRedirectUrlIfNotSetSpy).not.toHaveBeenCalled(); + }); + + it('keeps the query string of the originating page', () => { + setQueryParams({ redirectUrl: 'http://dev-6.pc:8603/repository/search?query=test' }); + + component.submit(); + + expect(setRedirectUrlSpy).toHaveBeenCalledWith('/repository/search?query=test'); + }); + + it('prefers a nested redirectUrl so the login page is not the redirect target', () => { + setQueryParams({ + redirectUrl: 'http://dev-6.pc:8603/repository/login?redirectUrl=http://dev-6.pc:8603/repository/items/1', + }); + + component.submit(); + + expect(setRedirectUrlSpy).toHaveBeenCalledWith('/repository/items/1'); + }); + + it('passes through an already-relative redirectUrl unchanged', () => { + setQueryParams({ redirectUrl: '/repository/search' }); + + component.submit(); + + expect(setRedirectUrlSpy).toHaveBeenCalledWith('/repository/search'); + }); + + it('falls back to setRedirectUrlIfNotSet("/") when no redirectUrl query param is present', () => { + setQueryParams({}); + + component.submit(); + + expect(setRedirectUrlIfNotSetSpy).toHaveBeenCalledWith('/'); + expect(setRedirectUrlSpy).not.toHaveBeenCalled(); + }); + + it('falls back cleanly when redirectUrl is not a string (repeated query param)', () => { + setQueryParams({ redirectUrl: ['/repository/a', '/repository/b'] }); + + component.submit(); + + expect(setRedirectUrlIfNotSetSpy).toHaveBeenCalledWith('/'); + expect(setRedirectUrlSpy).not.toHaveBeenCalled(); + }); + }); + }); /** diff --git a/src/app/shared/log-in/methods/password/log-in-password.component.ts b/src/app/shared/log-in/methods/password/log-in-password.component.ts index 0b18adbac07..1177bf6301a 100644 --- a/src/app/shared/log-in/methods/password/log-in-password.component.ts +++ b/src/app/shared/log-in/methods/password/log-in-password.component.ts @@ -17,7 +17,10 @@ import { UntypedFormGroup, Validators, } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { + ActivatedRoute, + RouterLink, +} from '@angular/router'; import { select, Store, @@ -145,6 +148,7 @@ export class LogInPasswordComponent implements OnInit, OnDestroy { @Inject('isStandalonePage') public isStandalonePage: boolean, private authService: AuthService, private hardRedirectService: HardRedirectService, + private route: ActivatedRoute, private formBuilder: UntypedFormBuilder, protected store: Store, protected authorizationService: AuthorizationDataService, @@ -239,7 +243,13 @@ export class LogInPasswordComponent implements OnInit, OnDestroy { if (!this.isStandalonePage) { this.authService.setRedirectUrl(this.hardRedirectService.getCurrentRoute()); } else { - this.authService.setRedirectUrlIfNotSet('/'); + // Standalone login: return to the `redirectUrl` query param set by the aai.js local-auth flow. + const redirectUrl = this.getRedirectUrlFromQueryParams(); + if (isNotEmpty(redirectUrl)) { + this.authService.setRedirectUrl(redirectUrl); + } else { + this.authService.setRedirectUrlIfNotSet('/'); + } } // dispatch AuthenticationAction @@ -249,6 +259,26 @@ export class LogInPasswordComponent implements OnInit, OnDestroy { this.form.reset(); } + /** Post-login redirect target from the `redirectUrl` query param (aai.js), as an app-relative path or null. */ + private getRedirectUrlFromQueryParams(): string | null { + // Query params are untyped (can be a string[]); only a non-empty string is usable here. + const rawRedirectUrl = this.route.snapshot.queryParams?.redirectUrl; + if (typeof rawRedirectUrl !== 'string' || isEmpty(rawRedirectUrl)) { + return null; + } + + // Prefer a nested `redirectUrl` so login is never the redirect target. + const nestedRedirectUrl = new URLSearchParams(rawRedirectUrl.split('?')[1] ?? '').get('redirectUrl'); + const redirectUrl = isNotEmpty(nestedRedirectUrl) ? nestedRedirectUrl : rawRedirectUrl; + + return this.toRelativePath(redirectUrl); + } + + /** Reduce a possibly-absolute URL to an app-relative path by dropping the scheme+host; relative values pass through. */ + private toRelativePath(url: string): string { + return url.replace(/^https?:\/\/[^/]+/i, ''); + } + /** * Toggle Discojuice login. Show it every time except the case when the user click * on the `local` button in the discojuice box.