72699: Hard redirect after log in - loading fixes

This commit is contained in:
Kristof De Langhe
2020-08-27 14:50:13 +02:00
parent 7fbae8997d
commit 55c45f5f6c
25 changed files with 204 additions and 146 deletions

View File

@@ -1,4 +1,7 @@
<div class="outer-wrapper"> <div class="text-center ds-full-screen-loader d-flex align-items-center flex-column justify-content-center" *ngIf="!(hasAuthFinishedLoading$ | async)">
<ds-loading [showMessage]="false"></ds-loading>
</div>
<div class="outer-wrapper" *ngIf="hasAuthFinishedLoading$ | async">
<ds-admin-sidebar></ds-admin-sidebar> <ds-admin-sidebar></ds-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{ <div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'), value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),

View File

@@ -47,3 +47,7 @@ ds-admin-sidebar {
position: fixed; position: fixed;
z-index: $sidebar-z-index; z-index: $sidebar-z-index;
} }
.ds-full-screen-loader {
height: 100vh;
}

View File

@@ -1,9 +1,8 @@
import * as ngrx from '@ngrx/store';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { By } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
@@ -30,13 +29,13 @@ import { RouteService } from './core/services/route.service';
import { MockActivatedRoute } from './shared/mocks/active-router.mock'; import { MockActivatedRoute } from './shared/mocks/active-router.mock';
import { RouterMock } from './shared/mocks/router.mock'; import { RouterMock } from './shared/mocks/router.mock';
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
import { storeModuleConfig } from './app.reducer'; import { AppState, storeModuleConfig } from './app.reducer';
import { LocaleService } from './core/locale/locale.service'; import { LocaleService } from './core/locale/locale.service';
import { authReducer } from './core/auth/auth.reducer';
import { cold } from 'jasmine-marbles';
let comp: AppComponent; let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>; let fixture: ComponentFixture<AppComponent>;
let de: DebugElement;
let el: HTMLElement;
const menuService = new MenuServiceStub(); const menuService = new MenuServiceStub();
describe('App component', () => { describe('App component', () => {
@@ -52,7 +51,7 @@ describe('App component', () => {
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
StoreModule.forRoot({}, storeModuleConfig), StoreModule.forRoot(authReducer, storeModuleConfig),
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
@@ -82,12 +81,19 @@ describe('App component', () => {
// synchronous beforeEach // synchronous beforeEach
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AppComponent); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => cold('a', {
a: {
core: { auth: { loading: false } }
}
})
};
});
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance; // component test instance comp = fixture.componentInstance; // component test instance
// query for the <div class='outer-wrapper'> by CSS element selector fixture.detectChanges();
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
el = de.nativeElement;
}); });
it('should create component', inject([AppComponent], (app: AppComponent) => { it('should create component', inject([AppComponent], (app: AppComponent) => {

View File

@@ -1,4 +1,4 @@
import { delay, filter, map, take } from 'rxjs/operators'; import { delay, filter, map, take, distinctUntilChanged } from 'rxjs/operators';
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
@@ -19,7 +19,7 @@ import { MetadataService } from './core/metadata/metadata.service';
import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowResizeAction } from './shared/host-window.actions';
import { HostWindowState } from './shared/search/host-window.reducer'; import { HostWindowState } from './shared/search/host-window.reducer';
import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
import { isAuthenticated } from './core/auth/selectors'; import { isAuthenticated, isAuthenticationLoading } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service'; import { AuthService } from './core/auth/auth.service';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service'; import { MenuService } from './shared/menu/menu.service';
@@ -52,6 +52,11 @@ export class AppComponent implements OnInit, AfterViewInit {
notificationOptions = environment.notifications; notificationOptions = environment.notifications;
models; models;
/**
* Whether or not the authenticated has finished loading
*/
hasAuthFinishedLoading$: Observable<boolean>;
constructor( constructor(
@Inject(NativeWindowService) private _window: NativeWindowRef, @Inject(NativeWindowService) private _window: NativeWindowRef,
private translate: TranslateService, private translate: TranslateService,
@@ -89,6 +94,10 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
ngOnInit() { ngOnInit() {
this.hasAuthFinishedLoading$ = this.store.pipe(select(isAuthenticationLoading)).pipe(
map((isLoading: boolean) => isLoading === false),
distinctUntilChanged()
);
const env: string = environment.production ? 'Production' : 'Development'; const env: string = environment.production ? 'Production' : 'Development';
const color: string = environment.production ? 'red' : 'green'; const color: string = environment.production ? 'red' : 'green';
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);

View File

@@ -34,6 +34,7 @@ export const AuthActionTypes = {
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'), RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'), RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'), RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS')
}; };
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@@ -335,6 +336,20 @@ export class SetRedirectUrlAction implements Action {
} }
} }
/**
* Start loading for a hard redirect
* @class StartHardRedirectLoadingAction
* @implements {Action}
*/
export class RedirectAfterLoginSuccessAction implements Action {
public type: string = AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS;
payload: string;
constructor(url: string) {
this.payload = url;
}
}
/** /**
* Retrieve the authenticated eperson. * Retrieve the authenticated eperson.
* @class RetrieveAuthenticatedEpersonAction * @class RetrieveAuthenticatedEpersonAction
@@ -402,8 +417,8 @@ export type AuthActions
| RetrieveAuthMethodsSuccessAction | RetrieveAuthMethodsSuccessAction
| RetrieveAuthMethodsErrorAction | RetrieveAuthMethodsErrorAction
| RetrieveTokenAction | RetrieveTokenAction
| ResetAuthenticationMessagesAction
| RetrieveAuthenticatedEpersonAction | RetrieveAuthenticatedEpersonAction
| RetrieveAuthenticatedEpersonErrorAction | RetrieveAuthenticatedEpersonErrorAction
| RetrieveAuthenticatedEpersonSuccessAction | RetrieveAuthenticatedEpersonSuccessAction
| SetRedirectUrlAction; | SetRedirectUrlAction
| RedirectAfterLoginSuccessAction;

View File

@@ -27,6 +27,7 @@ import {
CheckAuthenticationTokenCookieAction, CheckAuthenticationTokenCookieAction,
LogOutErrorAction, LogOutErrorAction,
LogOutSuccessAction, LogOutSuccessAction,
RedirectAfterLoginSuccessAction,
RefreshTokenAction, RefreshTokenAction,
RefreshTokenErrorAction, RefreshTokenErrorAction,
RefreshTokenSuccessAction, RefreshTokenSuccessAction,
@@ -79,7 +80,26 @@ export class AuthEffects {
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe( public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS), ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)), tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)),
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref)) switchMap((action: AuthenticatedSuccessAction) => this.authService.getRedirectUrl().pipe(
take(1),
map((redirectUrl: string) => [action, redirectUrl])
)),
map(([action, redirectUrl]: [AuthenticatedSuccessAction, string]) => {
if (hasValue(redirectUrl)) {
return new RedirectAfterLoginSuccessAction(redirectUrl);
} else {
return new RetrieveAuthenticatedEpersonAction(action.payload.userHref);
}
})
);
@Effect({ dispatch: false })
public redirectAfterLoginSuccess$: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS),
tap((action: RedirectAfterLoginSuccessAction) => {
this.authService.clearRedirectUrl();
this.authService.navigateToRedirectUrl(action.payload);
})
); );
// It means "reacts to this action but don't send another" // It means "reacts to this action but don't send another"

View File

@@ -62,7 +62,7 @@ export interface AuthState {
const initialState: AuthState = { const initialState: AuthState = {
authenticated: false, authenticated: false,
loaded: false, loaded: false,
loading: false, loading: undefined,
authMethods: [] authMethods: []
}; };
@@ -201,6 +201,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
redirectUrl: (action as SetRedirectUrlAction).payload, redirectUrl: (action as SetRedirectUrlAction).payload,
}); });
case AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS:
return Object.assign({}, state, {
loading: true,
});
default: default:
return state; return state;
} }

View File

@@ -323,35 +323,30 @@ describe('AuthService test', () => {
}); });
it('should set redirect url to previous page', () => { it('should set redirect url to previous page', () => {
spyOn(routeServiceMock, 'getHistory').and.callThrough(); (storage.get as jasmine.Spy).and.returnValue('/collection/123');
authService.redirectAfterLoginSuccess(true); authService.redirectAfterLoginSuccess();
expect(routeServiceMock.getHistory).toHaveBeenCalled();
// Reload with redirect URL set to /collection/123 // Reload with redirect URL set to /collection/123
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123')))); expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123'))));
}); });
it('should set redirect url to current page', () => { it('should set redirect url to current page', () => {
spyOn(routeServiceMock, 'getHistory').and.callThrough(); (storage.get as jasmine.Spy).and.returnValue('/home');
authService.redirectAfterLoginSuccess(false); authService.redirectAfterLoginSuccess();
expect(routeServiceMock.getHistory).toHaveBeenCalled();
// Reload with redirect URL set to /home // Reload with redirect URL set to /home
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/home')))); expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/home'))));
}); });
it('should redirect to / and not to /login', () => { it('should redirect to regular reload and not to /login', () => {
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login'])); (storage.get as jasmine.Spy).and.returnValue('/login');
authService.redirectAfterLoginSuccess(true); authService.redirectAfterLoginSuccess();
expect(routeServiceMock.getHistory).toHaveBeenCalled();
// Reload without a redirect URL // Reload without a redirect URL
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$'))); expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$')));
}); });
it('should redirect to / when no redirect url is found', () => { it('should not redirect when no redirect url is found', () => {
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf([''])); authService.redirectAfterLoginSuccess();
authService.redirectAfterLoginSuccess(true);
expect(routeServiceMock.getHistory).toHaveBeenCalled();
// Reload without a redirect URL // Reload without a redirect URL
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$'))); expect(hardRedirectService.redirect).not.toHaveBeenCalled();
}); });
describe('impersonate', () => { describe('impersonate', () => {

View File

@@ -14,7 +14,15 @@ import { AuthRequestService } from './auth-request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; import {
hasValue,
hasValueOperator,
isEmpty,
isNotEmpty,
isNotNull,
isNotUndefined,
hasNoValue
} from '../../shared/empty.util';
import { CookieService } from '../services/cookie.service'; import { CookieService } from '../services/cookie.service';
import { import {
getAuthenticatedUserId, getAuthenticatedUserId,
@@ -413,35 +421,19 @@ export class AuthService {
/** /**
* Redirect to the route navigated before the login * Redirect to the route navigated before the login
*/ */
public redirectAfterLoginSuccess(isStandalonePage: boolean) { public redirectAfterLoginSuccess() {
this.getRedirectUrl().pipe( this.getRedirectUrl().pipe(
take(1)) take(1))
.subscribe((redirectUrl) => { .subscribe((redirectUrl) => {
if (hasValue(redirectUrl)) {
if (isNotEmpty(redirectUrl)) {
this.clearRedirectUrl(); this.clearRedirectUrl();
this.router.onSameUrlNavigation = 'reload';
this.navigateToRedirectUrl(redirectUrl); this.navigateToRedirectUrl(redirectUrl);
} else {
// If redirectUrl is empty use history.
this.routeService.getHistory().pipe(
take(1)
).subscribe((history) => {
let redirUrl;
if (isStandalonePage) {
// For standalone login pages, use the previous route.
redirUrl = history[history.length - 2] || '';
} else {
redirUrl = history[history.length - 1] || '';
}
this.navigateToRedirectUrl(redirUrl);
});
} }
}); });
} }
protected navigateToRedirectUrl(redirectUrl: string) { public navigateToRedirectUrl(redirectUrl: string) {
let url = `/reload/${new Date().getTime()}`; let url = `/reload/${new Date().getTime()}`;
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) { if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
url += `?redirect=${encodeURIComponent(redirectUrl)}`; url += `?redirect=${encodeURIComponent(redirectUrl)}`;
@@ -460,12 +452,16 @@ export class AuthService {
* Get redirect url * Get redirect url
*/ */
getRedirectUrl(): Observable<string> { getRedirectUrl(): Observable<string> {
const redirectUrl = this.storage.get(REDIRECT_COOKIE); return this.store.pipe(
if (isNotEmpty(redirectUrl)) { select(getRedirectUrl),
return observableOf(redirectUrl); map((urlFromStore: string) => {
if (hasValue(urlFromStore)) {
return urlFromStore;
} else { } else {
return this.store.pipe(select(getRedirectUrl)); return this.storage.get(REDIRECT_COOKIE);
} }
})
);
} }
/** /**
@@ -482,6 +478,16 @@ export class AuthService {
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : '')); this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
} }
setRedirectUrlIfNotSet(newRedirectUrl: string) {
this.getRedirectUrl().pipe(
take(1))
.subscribe((currentRedirectUrl) => {
if (hasNoValue(currentRedirectUrl)) {
this.setRedirectUrl(newRedirectUrl);
}
})
}
/** /**
* Clear redirect url * Clear redirect url
*/ */

View File

@@ -9,11 +9,11 @@ import {
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map, find, switchMap } from 'rxjs/operators';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { isAuthenticated } from './selectors'; import { isAuthenticated, isAuthenticationLoading } from './selectors';
import { AuthService, LOGIN_ROUTE } from './auth.service'; import { AuthService, LOGIN_ROUTE } from './auth.service';
/** /**
@@ -48,11 +48,10 @@ export class AuthenticatedGuard implements CanActivate {
} }
private handleAuth(url: string): Observable<boolean | UrlTree> { private handleAuth(url: string): Observable<boolean | UrlTree> {
// get observable
const observable = this.store.pipe(select(isAuthenticated));
// redirect to sign in page if user is not authenticated // redirect to sign in page if user is not authenticated
return observable.pipe( return this.store.pipe(select(isAuthenticationLoading)).pipe(
find((isLoading: boolean) => isLoading === false),
switchMap(() => this.store.pipe(select(isAuthenticated))),
map((authenticated) => { map((authenticated) => {
if (authenticated) { if (authenticated) {
return authenticated; return authenticated;

View File

@@ -58,32 +58,4 @@ export class ServerAuthService extends AuthService {
map((status: AuthStatus) => Object.assign(new AuthStatus(), status)) map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
); );
} }
/**
* Redirect to the route navigated before the login
*/
public redirectAfterLoginSuccess(isStandalonePage: boolean) {
this.getRedirectUrl().pipe(
take(1))
.subscribe((redirectUrl) => {
if (isNotEmpty(redirectUrl)) {
// override the route reuse strategy
this.router.routeReuseStrategy.shouldReuseRoute = () => {
return false;
};
this.router.navigated = false;
const url = decodeURIComponent(redirectUrl);
this.router.navigateByUrl(url);
} else {
// If redirectUrl is empty use history. For ssr the history array should contain the requested url.
this.routeService.getHistory().pipe(
filter((history) => history.length > 0),
take(1)
).subscribe((history) => {
this.navigateToRedirectUrl(history[history.length - 1] || '');
});
}
})
}
} }

View File

@@ -5,7 +5,8 @@ describe('BrowserHardRedirectService', () => {
const mockLocation = { const mockLocation = {
href: undefined, href: undefined,
origin: 'test origin', pathname: '/pathname',
search: '/search',
} as Location; } as Location;
const service: BrowserHardRedirectService = new BrowserHardRedirectService(mockLocation); const service: BrowserHardRedirectService = new BrowserHardRedirectService(mockLocation);
@@ -31,10 +32,10 @@ describe('BrowserHardRedirectService', () => {
}) })
}); });
describe('when requesting the origin', () => { describe('when requesting the current route', () => {
it('should return the location origin', () => { it('should return the location origin', () => {
expect(service.getOriginFromUrl()).toEqual('test origin'); expect(service.getCurrentRoute()).toEqual(mockLocation.pathname + mockLocation.search);
}); });
}); });
}); });

View File

@@ -1,11 +1,12 @@
import {Inject, Injectable} from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import {LocationToken} from '../../../modules/app/browser-app.module'; import { LocationToken } from '../../../modules/app/browser-app.module';
import { HardRedirectService } from './hard-redirect.service';
/** /**
* Service for performing hard redirects within the browser app module * Service for performing hard redirects within the browser app module
*/ */
@Injectable() @Injectable()
export class BrowserHardRedirectService { export class BrowserHardRedirectService implements HardRedirectService {
constructor( constructor(
@Inject(LocationToken) protected location: Location, @Inject(LocationToken) protected location: Location,
@@ -23,7 +24,7 @@ export class BrowserHardRedirectService {
/** /**
* Get the origin of a request * Get the origin of a request
*/ */
getOriginFromUrl() { getCurrentRoute() {
return this.location.origin; return this.location.pathname + this.location.search;
} }
} }

View File

@@ -15,7 +15,8 @@ export abstract class HardRedirectService {
abstract redirect(url: string); abstract redirect(url: string);
/** /**
* Get the origin of a request * Get the current route, with query params included
* e.g. /search?page=1&query=open%20access&f.dateIssued.min=1980&f.dateIssued.max=2020
*/ */
abstract getOriginFromUrl(); abstract getCurrentRoute();
} }

View File

@@ -30,19 +30,14 @@ describe('ServerHardRedirectService', () => {
}) })
}); });
describe('when requesting the origin', () => { describe('when requesting the current route', () => {
beforeEach(() => { beforeEach(() => {
mockRequest.protocol = 'test-protocol'; mockRequest.originalUrl = 'original/url';
mockRequest.get.and.callFake((name) => {
if (name === 'hostname') {
return 'test-host';
}
});
}); });
it('should return the location origin', () => { it('should return the location origin', () => {
expect(service.getOriginFromUrl()).toEqual('test-protocol://test-host'); expect(service.getCurrentRoute()).toEqual(mockRequest.originalUrl);
}); });
}); });
}); });

View File

@@ -1,12 +1,13 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { HardRedirectService } from './hard-redirect.service';
/** /**
* Service for performing hard redirects within the server app module * Service for performing hard redirects within the server app module
*/ */
@Injectable() @Injectable()
export class ServerHardRedirectService { export class ServerHardRedirectService implements HardRedirectService {
constructor( constructor(
@Inject(REQUEST) protected req: Request, @Inject(REQUEST) protected req: Request,
@@ -55,8 +56,7 @@ export class ServerHardRedirectService {
/** /**
* Get the origin of a request * Get the origin of a request
*/ */
getOriginFromUrl() { getCurrentRoute() {
return this.req.originalUrl;
return new URL(`${this.req.protocol}://${this.req.get('hostname')}`).toString();
} }
} }

View File

@@ -12,6 +12,7 @@ import { AuthService } from '../../../core/auth/auth.service';
import { AuthMethod } from '../../../core/auth/models/auth.method'; import { AuthMethod } from '../../../core/auth/models/auth.method';
import { AuthServiceStub } from '../../testing/auth-service.stub'; import { AuthServiceStub } from '../../testing/auth-service.stub';
import { createTestComponent } from '../../testing/utils.test'; import { createTestComponent } from '../../testing/utils.test';
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
describe('LogInContainerComponent', () => { describe('LogInContainerComponent', () => {
@@ -20,7 +21,13 @@ describe('LogInContainerComponent', () => {
const authMethod = new AuthMethod('password'); const authMethod = new AuthMethod('password');
let hardRedirectService: HardRedirectService;
beforeEach(async(() => { beforeEach(async(() => {
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
redirect: {},
getCurrentRoute: {}
});
// refine the test module by declaring the test component // refine the test module by declaring the test component
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -35,6 +42,7 @@ describe('LogInContainerComponent', () => {
], ],
providers: [ providers: [
{provide: AuthService, useClass: AuthServiceStub}, {provide: AuthService, useClass: AuthServiceStub},
{ provide: HardRedirectService, useValue: hardRedirectService },
LogInContainerComponent LogInContainerComponent
], ],
schemas: [ schemas: [

View File

@@ -18,6 +18,7 @@ import { NativeWindowService } from '../../core/services/window.service';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { createTestComponent } from '../testing/utils.test'; import { createTestComponent } from '../testing/utils.test';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
describe('LogInComponent', () => { describe('LogInComponent', () => {
@@ -33,8 +34,13 @@ describe('LogInComponent', () => {
} }
} }
}; };
let hardRedirectService: HardRedirectService;
beforeEach(async(() => { beforeEach(async(() => {
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
redirect: {},
getCurrentRoute: {}
});
// refine the test module by declaring the test component // refine the test module by declaring the test component
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -58,6 +64,7 @@ describe('LogInComponent', () => {
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory }, { provide: NativeWindowService, useFactory: NativeWindowMockFactory },
// { provide: Router, useValue: new RouterStub() }, // { provide: Router, useValue: new RouterStub() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: HardRedirectService, useValue: hardRedirectService },
provideMockStore({ initialState }), provideMockStore({ initialState }),
LogInComponent LogInComponent
], ],

View File

@@ -1,13 +1,9 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, takeWhile, } from 'rxjs/operators';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { AuthMethod } from '../../core/auth/models/auth.method'; import { AuthMethod } from '../../core/auth/models/auth.method';
import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
import { CoreState } from '../../core/core.reducers'; import { CoreState } from '../../core/core.reducers';
import { AuthService } from '../../core/auth/auth.service';
import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module'; import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module';
/** /**
@@ -19,7 +15,7 @@ import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module
templateUrl: './log-in.component.html', templateUrl: './log-in.component.html',
styleUrls: ['./log-in.component.scss'] styleUrls: ['./log-in.component.scss']
}) })
export class LogInComponent implements OnInit, OnDestroy { export class LogInComponent implements OnInit {
/** /**
* A boolean representing if LogInComponent is in a standalone page * A boolean representing if LogInComponent is in a standalone page
@@ -45,14 +41,7 @@ export class LogInComponent implements OnInit, OnDestroy {
*/ */
public loading: Observable<boolean>; public loading: Observable<boolean>;
/** constructor(private store: Store<CoreState>) {
* Component state.
* @type {boolean}
*/
private alive = true;
constructor(private store: Store<CoreState>,
private authService: AuthService,) {
} }
ngOnInit(): void { ngOnInit(): void {
@@ -66,21 +55,6 @@ export class LogInComponent implements OnInit, OnDestroy {
// set isAuthenticated // set isAuthenticated
this.isAuthenticated = this.store.pipe(select(isAuthenticated)); this.isAuthenticated = this.store.pipe(select(isAuthenticated));
// subscribe to success
this.store.pipe(
select(isAuthenticated),
takeWhile(() => this.alive),
filter((authenticated) => authenticated))
.subscribe(() => {
this.authService.redirectAfterLoginSuccess(this.isStandalonePage);
}
);
}
ngOnDestroy(): void {
this.alive = false;
} }
getRegisterPath() { getRegisterPath() {

View File

@@ -15,6 +15,7 @@ import { AuthServiceStub } from '../../../testing/auth-service.stub';
import { AppState } from '../../../../app.reducer'; import { AppState } from '../../../../app.reducer';
import { AuthMethod } from '../../../../core/auth/models/auth.method'; import { AuthMethod } from '../../../../core/auth/models/auth.method';
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
describe('LogInPasswordComponent', () => { describe('LogInPasswordComponent', () => {
@@ -29,8 +30,14 @@ describe('LogInPasswordComponent', () => {
loading: false, loading: false,
}; };
let hardRedirectService: HardRedirectService;
beforeEach(() => { beforeEach(() => {
user = EPersonMock; user = EPersonMock;
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
getCurrentRoute: {}
});
}); });
beforeEach(async(() => { beforeEach(async(() => {
@@ -47,7 +54,8 @@ describe('LogInPasswordComponent', () => {
], ],
providers: [ providers: [
{ provide: AuthService, useClass: AuthServiceStub }, { provide: AuthService, useClass: AuthServiceStub },
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) } { provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) },
{ provide: HardRedirectService, useValue: hardRedirectService },
], ],
schemas: [ schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA

View File

@@ -13,6 +13,8 @@ import { fadeOut } from '../../../animations/fade';
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
import { renderAuthMethodFor } from '../log-in.methods-decorator'; import { renderAuthMethodFor } from '../log-in.methods-decorator';
import { AuthMethod } from '../../../../core/auth/models/auth.method'; import { AuthMethod } from '../../../../core/auth/models/auth.method';
import { AuthService } from '../../../../core/auth/auth.service';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
/** /**
* /users/sign-in * /users/sign-in
@@ -66,11 +68,15 @@ export class LogInPasswordComponent implements OnInit {
/** /**
* @constructor * @constructor
* @param {AuthMethod} injectedAuthMethodModel * @param {AuthMethod} injectedAuthMethodModel
* @param {AuthService} authService
* @param {HardRedirectService} hardRedirectService
* @param {FormBuilder} formBuilder * @param {FormBuilder} formBuilder
* @param {Store<State>} store * @param {Store<State>} store
*/ */
constructor( constructor(
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
private authService: AuthService,
private hardRedirectService: HardRedirectService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private store: Store<CoreState> private store: Store<CoreState>
) { ) {
@@ -134,6 +140,8 @@ export class LogInPasswordComponent implements OnInit {
email.trim(); email.trim();
password.trim(); password.trim();
this.authService.setRedirectUrlIfNotSet(this.hardRedirectService.getCurrentRoute());
// dispatch AuthenticationAction // dispatch AuthenticationAction
this.store.dispatch(new AuthenticateAction(email, password)); this.store.dispatch(new AuthenticateAction(email, password));

View File

@@ -17,6 +17,7 @@ import { NativeWindowService } from '../../../../core/services/window.service';
import { RouterStub } from '../../../testing/router.stub'; import { RouterStub } from '../../../testing/router.stub';
import { ActivatedRouteStub } from '../../../testing/active-router.stub'; import { ActivatedRouteStub } from '../../../testing/active-router.stub';
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref'; import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
describe('LogInShibbolethComponent', () => { describe('LogInShibbolethComponent', () => {
@@ -30,6 +31,7 @@ describe('LogInShibbolethComponent', () => {
let location; let location;
let authState; let authState;
let hardRedirectService: HardRedirectService;
beforeEach(() => { beforeEach(() => {
user = EPersonMock; user = EPersonMock;
@@ -41,6 +43,10 @@ describe('LogInShibbolethComponent', () => {
loaded: false, loaded: false,
loading: false, loading: false,
}; };
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
getCurrentRoute: {}
});
}); });
beforeEach(async(() => { beforeEach(async(() => {
@@ -59,6 +65,7 @@ describe('LogInShibbolethComponent', () => {
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory }, { provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: new RouterStub() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: HardRedirectService, useValue: hardRedirectService },
], ],
schemas: [ schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA

View File

@@ -12,6 +12,8 @@ import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/
import { RouteService } from '../../../../core/services/route.service'; import { RouteService } from '../../../../core/services/route.service';
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
import { isNotNull } from '../../../empty.util'; import { isNotNull } from '../../../empty.util';
import { AuthService } from '../../../../core/auth/auth.service';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
@Component({ @Component({
selector: 'ds-log-in-shibboleth', selector: 'ds-log-in-shibboleth',
@@ -51,12 +53,16 @@ export class LogInShibbolethComponent implements OnInit {
* @param {AuthMethod} injectedAuthMethodModel * @param {AuthMethod} injectedAuthMethodModel
* @param {NativeWindowRef} _window * @param {NativeWindowRef} _window
* @param {RouteService} route * @param {RouteService} route
* @param {AuthService} authService
* @param {HardRedirectService} hardRedirectService
* @param {Store<State>} store * @param {Store<State>} store
*/ */
constructor( constructor(
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
@Inject(NativeWindowService) protected _window: NativeWindowRef, @Inject(NativeWindowService) protected _window: NativeWindowRef,
private route: RouteService, private route: RouteService,
private authService: AuthService,
private hardRedirectService: HardRedirectService,
private store: Store<CoreState> private store: Store<CoreState>
) { ) {
this.authMethod = injectedAuthMethodModel; this.authMethod = injectedAuthMethodModel;
@@ -75,6 +81,7 @@ export class LogInShibbolethComponent implements OnInit {
} }
redirectToShibboleth() { redirectToShibboleth() {
this.authService.setRedirectUrlIfNotSet(this.hardRedirectService.getCurrentRoute())
let newLocationUrl = this.location; let newLocationUrl = this.location;
const currentUrl = this._window.nativeWindow.location.href; const currentUrl = this._window.nativeWindow.location.href;
const myRegexp = /\?redirectUrl=(.*)/g; const myRegexp = /\?redirectUrl=(.*)/g;

View File

@@ -154,4 +154,12 @@ export class AuthServiceStub {
resetAuthenticationError() { resetAuthenticationError() {
return; return;
} }
setRedirectUrlIfNotSet(url: string) {
return;
}
redirectAfterLoginSuccess() {
return;
}
} }

View File

@@ -40,7 +40,6 @@ export function locationProvider(): Location {
return window.location; return window.location;
} }
@NgModule({ @NgModule({
bootstrap: [AppComponent], bootstrap: [AppComponent],
imports: [ imports: [