Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hard redirect after log in #856

Merged
merged 19 commits into from Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
83 changes: 44 additions & 39 deletions 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';
Expand All @@ -17,49 +18,53 @@ import {
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
import { ReloadGuard } from './core/reload/reload.guard';

@NgModule({
imports: [
RouterModule.forRoot([
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'reload/:rnd', redirectTo: '/home', pathMatch: 'full' },
{ 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]
},
{ 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] },
{ 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]
},
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
{ 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]
},
{ 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] },
{ 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]
},
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
{ path: UNAUTHORIZED_PATH, component: UnauthorizedComponent },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
]}
],
{
onSameUrlNavigation: 'reload',
})
Expand Down
7 changes: 6 additions & 1 deletion src/app/app.component.html
@@ -1,4 +1,4 @@
<div class="outer-wrapper">
<div class="outer-wrapper" *ngIf="isNotAuthBlocking$ | async; else authLoader">
<ds-admin-sidebar></ds-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
Expand All @@ -23,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>
4 changes: 4 additions & 0 deletions src/app/app.component.scss
Expand Up @@ -47,3 +47,7 @@ ds-admin-sidebar {
position: fixed;
z-index: $sidebar-z-index;
}

.ds-full-screen-loader {
height: 100vh;
}
26 changes: 16 additions & 10 deletions src/app/app.component.spec.ts
@@ -1,9 +1,8 @@
import * as ngrx from '@ngrx/store';
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 { By } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';

import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Store, StoreModule } from '@ngrx/store';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
Expand Down Expand Up @@ -32,11 +31,11 @@ import { RouterMock } from './shared/mocks/router.mock';
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
import { storeModuleConfig } from './app.reducer';
import { LocaleService } from './core/locale/locale.service';
import { authReducer } from './core/auth/auth.reducer';
import { cold } from 'jasmine-marbles';

let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let de: DebugElement;
let el: HTMLElement;
const menuService = new MenuServiceStub();

describe('App component', () => {
Expand All @@ -52,7 +51,7 @@ describe('App component', () => {
return TestBed.configureTestingModule({
imports: [
CommonModule,
StoreModule.forRoot({}, storeModuleConfig),
StoreModule.forRoot(authReducer, storeModuleConfig),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
Expand Down Expand Up @@ -82,12 +81,19 @@ describe('App component', () => {

// synchronous 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
// query for the <div class='outer-wrapper'> by CSS element selector
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
el = de.nativeElement;
fixture.detectChanges();
});

it('should create component', inject([AppComponent], (app: AppComponent) => {
Expand Down
18 changes: 11 additions & 7 deletions src/app/app.component.ts
@@ -1,4 +1,4 @@
import { delay, filter, map, take } from 'rxjs/operators';
import { delay, map, distinctUntilChanged } from 'rxjs/operators';
import {
AfterViewInit,
ChangeDetectionStrategy,
Expand All @@ -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 } from './core/auth/selectors';
import { isAuthenticationBlocking } 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';
Expand Down Expand Up @@ -52,6 +52,11 @@ export class AppComponent implements OnInit, AfterViewInit {
notificationOptions = environment.notifications;
models;

/**
* Whether or not the authentication is currently blocking the UI
*/
isNotAuthBlocking$: Observable<boolean>;

constructor(
@Inject(NativeWindowService) private _window: NativeWindowRef,
private translate: TranslateService,
Expand Down Expand Up @@ -89,16 +94,15 @@ export class AppComponent implements OnInit, AfterViewInit {
}

ngOnInit() {
this.isNotAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged()
);
const env: string = environment.production ? 'Production' : 'Development';
const color: string = environment.production ? 'red' : 'green';
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');
Expand Down
14 changes: 12 additions & 2 deletions 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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
];

Expand Down
62 changes: 62 additions & 0 deletions 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<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 }));
});
});
});

});
31 changes: 31 additions & 0 deletions 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 } 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> {
tdonohue marked this conversation as resolved.
Show resolved Hide resolved
return this.store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged(),
filter((finished: boolean) => finished === true),
take(1),
);
}

}