Merge branch 'w2p-72699_Hard-redirect-after-log-in' into w2p-72541_User-agreement-and-Privacy-statement

Conflicts:
	src/app/app-routing.module.ts
This commit is contained in:
Kristof De Langhe
2020-09-03 10:49:06 +02:00
22 changed files with 303 additions and 67 deletions

View File

@@ -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',
})

View File

@@ -1,7 +1,4 @@
<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">
<div class="outer-wrapper" *ngIf="isAuthBlocking$ | async; else authLoader">
<ds-admin-sidebar></ds-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
@@ -26,3 +23,8 @@
<ds-footer></ds-footer>
</div>
</div>
<ng-template #authLoader>
<div class="text-center ds-full-screen-loader d-flex align-items-center flex-column justify-content-center">
<ds-loading [showMessage]="false"></ds-loading>
</div>
</ng-template>

View File

@@ -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<boolean>;
isAuthBlocking$: Observable<boolean>;
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');

View File

@@ -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<AppState>,) => {
return () => store.dispatch(new CheckAuthenticationTokenAction());
},
deps: [ Store ],
multi: true
},
...DYNAMIC_MATCHER_PROVIDERS,
];

View File

@@ -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<AppState>(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 }));
});
});
});
});

View File

@@ -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<AppState>) {
}
canActivate(): Observable<boolean> {
return this.store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged(),
filter((finished: boolean) => finished === true),
take(1),
);
}
}

View File

@@ -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)]
};

View File

@@ -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:

View File

@@ -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))

View File

@@ -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

View File

@@ -1,7 +1,7 @@
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
(click)="$event.stopPropagation();">
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle
class="px-1">{{ 'nav.login' | translate }}</a>
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu

View File

@@ -43,11 +43,13 @@ describe('AuthNavMenuComponent', () => {
notAuthState = {
authenticated: false,
loaded: false,
blocking: false,
loading: false
};
authState = {
authenticated: true,
loaded: true,
blocking: false,
loading: false,
authToken: new AuthTokenInfo('test_token'),
userId: EPersonMock.id

View File

@@ -34,6 +34,7 @@ describe('UserMenuComponent', () => {
authState = {
authenticated: true,
loaded: true,
blocking: false,
loading: false,
authToken: new AuthTokenInfo('test_token'),
userId: EPersonMock.id
@@ -41,6 +42,7 @@ describe('UserMenuComponent', () => {
authStateLoading = {
authenticated: true,
loaded: true,
blocking: false,
loading: true,
authToken: null,
userId: EPersonMock.id

View File

@@ -26,6 +26,7 @@ describe('ImpersonateNavbarComponent', () => {
authState = {
authenticated: true,
loaded: true,
blocking: false,
loading: false,
authToken: new AuthTokenInfo('test_token'),
userId: EPersonMock.id

View File

@@ -15,6 +15,12 @@ export class LogInContainerComponent implements OnInit {
@Input() authMethod: AuthMethod;
/**
* A boolean representing if LogInContainerComponent is in a standalone page
* @type {boolean}
*/
@Input() isStandalonePage: boolean;
/**
* Injector to inject a section component with the @Input parameters
* @type {Injector}
@@ -36,6 +42,7 @@ export class LogInContainerComponent implements OnInit {
this.objectInjector = Injector.create({
providers: [
{ provide: 'authMethodProvider', useFactory: () => (this.authMethod), deps: [] },
{ provide: 'isStandalonePage', useFactory: () => (this.isStandalonePage), deps: [] },
],
parent: this.injector
});

View File

@@ -4,7 +4,7 @@
<div *ngIf="i === 1" class="text-center mt-2">
<span class="align-middle">{{"login.form.or-divider" | translate}}</span>
</div>
<ds-log-in-container [authMethod]="authMethod"></ds-log-in-container>
<ds-log-in-container [authMethod]="authMethod" [isStandalonePage]="isStandalonePage"></ds-log-in-container>
</ng-container>
<div class="dropdown-divider"></div>

View File

@@ -2,9 +2,16 @@ import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { AuthMethod } from '../../core/auth/models/auth.method';
import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
import {
getAuthenticationError,
getAuthenticationMethods,
isAuthenticated,
isAuthenticationLoading
} from '../../core/auth/selectors';
import { CoreState } from '../../core/core.reducers';
import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module';
import { hasValue } from '../empty.util';
import { AuthService } from '../../core/auth/auth.service';
/**
* /users/sign-in
@@ -41,7 +48,8 @@ export class LogInComponent implements OnInit {
*/
public loading: Observable<boolean>;
constructor(private store: Store<CoreState>) {
constructor(private store: Store<CoreState>,
private authService: AuthService) {
}
ngOnInit(): void {
@@ -55,6 +63,13 @@ export class LogInComponent implements OnInit {
// set isAuthenticated
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
// Clear the redirect URL if an authentication error occurs and this is not a standalone page
this.store.pipe(select(getAuthenticationError)).subscribe((error) => {
if (hasValue(error) && !this.isStandalonePage) {
this.authService.clearRedirectUrl();
}
});
}
getRegisterPath() {

View File

@@ -55,6 +55,7 @@ describe('LogInPasswordComponent', () => {
providers: [
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) },
{ provide: 'isStandalonePage', useValue: true },
{ provide: HardRedirectService, useValue: hardRedirectService },
],
schemas: [

View File

@@ -68,6 +68,7 @@ export class LogInPasswordComponent implements OnInit {
/**
* @constructor
* @param {AuthMethod} injectedAuthMethodModel
* @param {boolean} isStandalonePage
* @param {AuthService} authService
* @param {HardRedirectService} hardRedirectService
* @param {FormBuilder} formBuilder
@@ -75,6 +76,7 @@ export class LogInPasswordComponent implements OnInit {
*/
constructor(
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
@Inject('isStandalonePage') public isStandalonePage: boolean,
private authService: AuthService,
private hardRedirectService: HardRedirectService,
private formBuilder: FormBuilder,
@@ -140,7 +142,11 @@ export class LogInPasswordComponent implements OnInit {
email.trim();
password.trim();
this.authService.setRedirectUrlIfNotSet(this.hardRedirectService.getCurrentRoute());
if (!this.isStandalonePage) {
this.authService.setRedirectUrl(this.hardRedirectService.getCurrentRoute());
} else {
this.authService.setRedirectUrlIfNotSet('/');
}
// dispatch AuthenticationAction
this.store.dispatch(new AuthenticateAction(email, password));

View File

@@ -62,6 +62,7 @@ describe('LogInShibbolethComponent', () => {
providers: [
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Shibboleth, location) },
{ provide: 'isStandalonePage', useValue: true },
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: Router, useValue: new RouterStub() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },

View File

@@ -51,6 +51,7 @@ export class LogInShibbolethComponent implements OnInit {
/**
* @constructor
* @param {AuthMethod} injectedAuthMethodModel
* @param {boolean} isStandalonePage
* @param {NativeWindowRef} _window
* @param {RouteService} route
* @param {AuthService} authService
@@ -59,6 +60,7 @@ export class LogInShibbolethComponent implements OnInit {
*/
constructor(
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
@Inject('isStandalonePage') public isStandalonePage: boolean,
@Inject(NativeWindowService) protected _window: NativeWindowRef,
private route: RouteService,
private authService: AuthService,
@@ -81,7 +83,11 @@ export class LogInShibbolethComponent implements OnInit {
}
redirectToShibboleth() {
this.authService.setRedirectUrlIfNotSet(this.hardRedirectService.getCurrentRoute())
if (!this.isStandalonePage) {
this.authService.setRedirectUrl(this.hardRedirectService.getCurrentRoute());
} else {
this.authService.setRedirectUrlIfNotSet('/');
}
let newLocationUrl = this.location;
const currentUrl = this._window.nativeWindow.location.href;
const myRegexp = /\?redirectUrl=(.*)/g;

View File

@@ -162,4 +162,8 @@ export class AuthServiceStub {
redirectAfterLoginSuccess() {
return;
}
clearRedirectUrl() {
return;
}
}