diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index bc0fa787c2..5e02a6cf1b 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; import { AuthenticatedGuard } from './core/auth/authenticated.guard'; @@ -95,46 +96,49 @@ export function getInfoModulePath() { @NgModule({ imports: [ RouterModule.forRoot([ - { path: '', redirectTo: '/home', pathMatch: 'full' }, - { path: 'reload/:rnd', component: PageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] }, - { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } }, - { path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' }, - { path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, - { path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, - { path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule' }, - { path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule' }, - { path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' }, - { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, - { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, - { path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule' }, - { - path: 'mydspace', - loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', - canActivate: [AuthenticatedGuard, EndUserAgreementGuard] - }, - { path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' }, - { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'}, - { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard, EndUserAgreementGuard] }, - { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, - { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, - { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' }, - { - path: 'workspaceitems', - loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' - }, - { - path: WORKFLOW_ITEM_MODULE_PATH, - loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' - }, - { - path: PROFILE_MODULE_PATH, - loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard, EndUserAgreementGuard] - }, - { path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementGuard] }, - { path: INFO_MODULE_PATH, loadChildren: './info/info.module#InfoModule' }, - { path: UNAUTHORIZED_PATH, component: UnauthorizedComponent }, - { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, - ], + { path: '', canActivate: [AuthBlockingGuard], + children: [ + { path: '', redirectTo: '/home', pathMatch: 'full' }, + { path: 'reload/:rnd', component: PageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] }, + { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } }, + { path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' }, + { path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, + { path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, + { path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule' }, + { path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule' }, + { path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' }, + { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, + { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, + { path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule' }, + { + path: 'mydspace', + loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', + canActivate: [AuthenticatedGuard, EndUserAgreementGuard] + }, + { path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' }, + { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'}, + { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard, EndUserAgreementGuard] }, + { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, + { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, + { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' }, + { + path: 'workspaceitems', + loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' + }, + { + path: WORKFLOW_ITEM_MODULE_PATH, + loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' + }, + { + path: PROFILE_MODULE_PATH, + loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard, EndUserAgreementGuard] + }, + { path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementGuard] }, + { path: INFO_MODULE_PATH, loadChildren: './info/info.module#InfoModule' }, + { path: UNAUTHORIZED_PATH, component: UnauthorizedComponent }, + { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, + ]} + ], { onSameUrlNavigation: 'reload', }) diff --git a/src/app/app.component.html b/src/app/app.component.html index 6d6f89ea35..b628424cd4 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,7 +1,4 @@ -
- -
-
+
+ +
+ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index d5488be610..3f03cd8dbe 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { delay, filter, map, take, distinctUntilChanged } from 'rxjs/operators'; +import { delay, map, distinctUntilChanged } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectionStrategy, @@ -19,7 +19,7 @@ import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/search/host-window.reducer'; import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; -import { isAuthenticated, isAuthenticationLoading } from './core/auth/selectors'; +import { isAuthenticationBlocking, isAuthenticationLoading } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.service'; @@ -55,7 +55,7 @@ export class AppComponent implements OnInit, AfterViewInit { /** * Whether or not the authenticated has finished loading */ - hasAuthFinishedLoading$: Observable; + isAuthBlocking$: Observable; constructor( @Inject(NativeWindowService) private _window: NativeWindowRef, @@ -94,8 +94,8 @@ export class AppComponent implements OnInit, AfterViewInit { } ngOnInit() { - this.hasAuthFinishedLoading$ = this.store.pipe(select(isAuthenticationLoading)).pipe( - map((isLoading: boolean) => isLoading === false), + this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe( + map((isBlocking: boolean) => isBlocking === false), distinctUntilChanged() ); const env: string = environment.production ? 'Production' : 'Development'; @@ -103,11 +103,6 @@ export class AppComponent implements OnInit, AfterViewInit { console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight); - // Whether is not authenticathed try to retrieve a possible stored auth token - this.store.pipe(select(isAuthenticated), - take(1), - filter((authenticated) => !authenticated) - ).subscribe((authenticated) => this.authService.checkAuthenticationToken()); this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN); this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth'); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 33454ed6c5..f1cdd5f2e5 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,11 +1,11 @@ import { APP_BASE_HREF, CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { EffectsModule } from '@ngrx/effects'; import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store'; -import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store'; +import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -21,6 +21,7 @@ import { AppComponent } from './app.component'; import { appEffects } from './app.effects'; import { appMetaReducers, debugMetaReducers } from './app.metareducers'; import { appReducers, AppState, storeModuleConfig } from './app.reducer'; +import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CoreModule } from './core/core.module'; import { ClientCookieService } from './core/services/client-cookie.service'; @@ -91,6 +92,15 @@ const PROVIDERS = [ useClass: DSpaceRouterStateSerializer }, ClientCookieService, + // Check the authentication token when the app initializes + { + provide: APP_INITIALIZER, + useFactory: (store: Store,) => { + return () => store.dispatch(new CheckAuthenticationTokenAction()); + }, + deps: [ Store ], + multi: true + }, ...DYNAMIC_MATCHER_PROVIDERS, ]; diff --git a/src/app/core/auth/auth-blocking.guard.spec.ts b/src/app/core/auth/auth-blocking.guard.spec.ts new file mode 100644 index 0000000000..2a89b01a85 --- /dev/null +++ b/src/app/core/auth/auth-blocking.guard.spec.ts @@ -0,0 +1,62 @@ +import { Store } from '@ngrx/store'; +import * as ngrx from '@ngrx/store'; +import { cold, getTestScheduler, initTestScheduler, resetTestScheduler } from 'jasmine-marbles/es6'; +import { of as observableOf } from 'rxjs'; +import { AppState } from '../../app.reducer'; +import { AuthBlockingGuard } from './auth-blocking.guard'; + +describe('AuthBlockingGuard', () => { + let guard: AuthBlockingGuard; + beforeEach(() => { + guard = new AuthBlockingGuard(new Store(undefined, undefined, undefined)); + initTestScheduler(); + }); + + afterEach(() => { + getTestScheduler().flush(); + resetTestScheduler(); + }); + + describe(`canActivate`, () => { + + describe(`when authState.loading is undefined`, () => { + beforeEach(() => { + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(undefined); + }; + }) + }); + it(`should not emit anything`, () => { + expect(guard.canActivate()).toBeObservable(cold('|')); + }); + }); + + describe(`when authState.loading is true`, () => { + beforeEach(() => { + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(true); + }; + }) + }); + it(`should not emit anything`, () => { + expect(guard.canActivate()).toBeObservable(cold('|')); + }); + }); + + describe(`when authState.loading is false`, () => { + beforeEach(() => { + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(false); + }; + }) + }); + it(`should succeed`, () => { + expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true })); + }); + }); + }); + +}); diff --git a/src/app/core/auth/auth-blocking.guard.ts b/src/app/core/auth/auth-blocking.guard.ts new file mode 100644 index 0000000000..7488c0c508 --- /dev/null +++ b/src/app/core/auth/auth-blocking.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; +import { select, Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, filter, map, take, tap } from 'rxjs/operators'; +import { AppState } from '../../app.reducer'; +import { isAuthenticationBlocking } from './selectors'; + +/** + * A guard that blocks the loading of any + * route until the authentication status has loaded. + * To ensure all rest requests get the correct auth header. + */ +@Injectable({ + providedIn: 'root' +}) +export class AuthBlockingGuard implements CanActivate { + + constructor(private store: Store) { + } + + canActivate(): Observable { + return this.store.pipe(select(isAuthenticationBlocking)).pipe( + map((isBlocking: boolean) => isBlocking === false), + distinctUntilChanged(), + filter((finished: boolean) => finished === true), + take(1), + ); + } + +} diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index cf934a7f47..649002903c 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -42,6 +42,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: true, loading: false, }; const action = new AuthenticateAction('user', 'password'); @@ -49,6 +50,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: true, error: undefined, loading: true, info: undefined @@ -62,6 +64,7 @@ describe('authReducer', () => { authenticated: false, loaded: false, error: undefined, + blocking: true, loading: true, info: undefined }; @@ -76,6 +79,7 @@ describe('authReducer', () => { authenticated: false, loaded: false, error: undefined, + blocking: true, loading: true, info: undefined }; @@ -84,6 +88,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: false, loading: false, info: undefined, authToken: undefined, @@ -96,6 +101,7 @@ describe('authReducer', () => { it('should properly set the state, in response to a AUTHENTICATED action', () => { initialState = { authenticated: false, + blocking: false, loaded: false, error: undefined, loading: true, @@ -103,8 +109,15 @@ describe('authReducer', () => { }; const action = new AuthenticatedAction(mockTokenInfo); const newState = authReducer(initialState, action); - - expect(newState).toEqual(initialState); + state = { + authenticated: false, + blocking: true, + loaded: false, + error: undefined, + loading: true, + info: undefined + }; + expect(newState).toEqual(state); }); it('should properly set the state, in response to a AUTHENTICATED_SUCCESS action', () => { @@ -112,6 +125,7 @@ describe('authReducer', () => { authenticated: false, loaded: false, error: undefined, + blocking: true, loading: true, info: undefined }; @@ -122,6 +136,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: false, error: undefined, + blocking: true, loading: true, info: undefined }; @@ -133,6 +148,7 @@ describe('authReducer', () => { authenticated: false, loaded: false, error: undefined, + blocking: true, loading: true, info: undefined }; @@ -143,6 +159,7 @@ describe('authReducer', () => { authToken: undefined, error: 'Test error message', loaded: true, + blocking: false, loading: false, info: undefined }; @@ -153,6 +170,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: false, loading: false, }; const action = new CheckAuthenticationTokenAction(); @@ -160,6 +178,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: true, loading: true, }; expect(newState).toEqual(state); @@ -169,6 +188,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: false, loading: true, }; const action = new CheckAuthenticationTokenCookieAction(); @@ -176,6 +196,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: true, loading: true, }; expect(newState).toEqual(state); @@ -187,6 +208,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id @@ -204,6 +226,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id @@ -216,6 +239,7 @@ describe('authReducer', () => { authToken: undefined, error: undefined, loaded: false, + blocking: false, loading: false, info: undefined, refreshing: false, @@ -230,6 +254,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id @@ -242,6 +267,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: 'Test error message', + blocking: false, loading: false, info: undefined, userId: EPersonMock.id @@ -255,6 +281,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: false, error: undefined, + blocking: true, loading: true, info: undefined }; @@ -265,6 +292,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id @@ -277,6 +305,7 @@ describe('authReducer', () => { authenticated: false, loaded: false, error: undefined, + blocking: true, loading: true, info: undefined }; @@ -287,6 +316,7 @@ describe('authReducer', () => { authToken: undefined, error: 'Test error message', loaded: true, + blocking: false, loading: false, info: undefined }; @@ -299,6 +329,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id @@ -311,6 +342,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id, @@ -325,6 +357,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id, @@ -338,6 +371,7 @@ describe('authReducer', () => { authToken: newTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id, @@ -352,6 +386,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id, @@ -364,6 +399,7 @@ describe('authReducer', () => { authToken: undefined, error: undefined, loaded: false, + blocking: false, loading: false, info: undefined, refreshing: false, @@ -378,6 +414,7 @@ describe('authReducer', () => { authToken: mockTokenInfo, loaded: true, error: undefined, + blocking: false, loading: false, info: undefined, userId: EPersonMock.id @@ -387,6 +424,7 @@ describe('authReducer', () => { authenticated: false, authToken: undefined, loaded: false, + blocking: false, loading: false, error: undefined, info: 'Message', @@ -410,6 +448,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: false, loading: false, }; const action = new AddAuthenticationMessageAction('Message'); @@ -417,6 +456,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: false, loading: false, info: 'Message' }; @@ -427,6 +467,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: false, loading: false, error: 'Error', info: 'Message' @@ -436,6 +477,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: false, loading: false, error: undefined, info: undefined @@ -447,6 +489,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: false, loading: false }; const action = new SetRedirectUrlAction('redirect.url'); @@ -454,6 +497,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: false, loading: false, redirectUrl: 'redirect.url' }; @@ -464,6 +508,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: false, loading: false, authMethods: [] }; @@ -472,6 +517,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: true, loading: true, authMethods: [] }; @@ -482,6 +528,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: true, loading: true, authMethods: [] }; @@ -494,6 +541,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: false, loading: false, authMethods: authMethods }; @@ -504,6 +552,7 @@ describe('authReducer', () => { initialState = { authenticated: false, loaded: false, + blocking: true, loading: true, authMethods: [] }; @@ -513,6 +562,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, + blocking: false, loading: false, authMethods: [new AuthMethod(AuthMethodType.Password)] }; diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 0ffd7d0519..9435dd1b1d 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -39,6 +39,10 @@ export interface AuthState { // true when loading loading: boolean; + // true when everything else should wait for authorization + // to complete + blocking: boolean; + // info message info?: string; @@ -62,7 +66,8 @@ export interface AuthState { const initialState: AuthState = { authenticated: false, loaded: false, - loading: undefined, + blocking: true, + loading: false, authMethods: [] }; @@ -86,7 +91,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN: case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE: return Object.assign({}, state, { - loading: true + loading: true, + blocking: true }); case AuthActionTypes.AUTHENTICATED_ERROR: @@ -96,6 +102,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut authToken: undefined, error: (action as AuthenticationErrorAction).payload.message, loaded: true, + blocking: false, loading: false }); @@ -110,6 +117,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loaded: true, error: undefined, loading: false, + blocking: false, info: undefined, userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload }); @@ -119,6 +127,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut authenticated: false, authToken: undefined, error: (action as AuthenticationErrorAction).payload.message, + blocking: false, loading: false }); @@ -139,6 +148,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut authToken: undefined, error: undefined, loaded: false, + blocking: false, loading: false, info: undefined, refreshing: false, @@ -151,6 +161,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut authenticated: false, authToken: undefined, loaded: false, + blocking: false, loading: false, info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload, userId: undefined @@ -181,18 +192,21 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut // next three cases are used by dynamic rendering of login methods case AuthActionTypes.RETRIEVE_AUTH_METHODS: return Object.assign({}, state, { - loading: true + loading: true, + blocking: true }); case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS: return Object.assign({}, state, { loading: false, + blocking: false, authMethods: (action as RetrieveAuthMethodsSuccessAction).payload }); case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR: return Object.assign({}, state, { loading: false, + blocking: false, authMethods: [new AuthMethod(AuthMethodType.Password)] }); @@ -204,6 +218,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS: return Object.assign({}, state, { loading: true, + blocking: true, }); default: diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 150e44296e..3671507cb0 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -436,6 +436,10 @@ export class AuthService { this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : '')); } + /** + * Set the redirect url if the current one has not been set yet + * @param newRedirectUrl + */ setRedirectUrlIfNotSet(newRedirectUrl: string) { this.getRedirectUrl().pipe( take(1)) diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts index 173f82e810..c4e95a0fb3 100644 --- a/src/app/core/auth/selectors.ts +++ b/src/app/core/auth/selectors.ts @@ -65,6 +65,14 @@ const _getAuthenticationInfo = (state: AuthState) => state.info; */ const _isLoading = (state: AuthState) => state.loading; +/** + * Returns true if everything else should wait for authentication. + * @function _isBlocking + * @param {State} state + * @returns {boolean} + */ +const _isBlocking = (state: AuthState) => state.blocking; + /** * Returns true if a refresh token request is in progress. * @function _isRefreshing @@ -170,6 +178,16 @@ export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticat */ export const isAuthenticationLoading = createSelector(getAuthState, _isLoading); +/** + * Returns true if the authentication should block everything else + * + * @function isAuthenticationBlocking + * @param {AuthState} state + * @param {any} props + * @return {boolean} + */ +export const isAuthenticationBlocking = createSelector(getAuthState, _isBlocking); + /** * Returns true if the refresh token request is loading. * @function isTokenRefreshing diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index a05381fee8..fa92939e0f 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -1,7 +1,7 @@