Skip to content

Commit

Permalink
Added the reader and admin guards [#547]
Browse files Browse the repository at this point in the history
  • Loading branch information
mcpierce committed Dec 11, 2020
1 parent 7ff5eba commit 00bb7c2
Show file tree
Hide file tree
Showing 11 changed files with 503 additions and 15 deletions.
7 changes: 7 additions & 0 deletions comixed-web/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import {
} from '@angular/material/menu';
import { APP_REDUCERS } from '@app/app.reducers';
import { SessionEffects } from '@app/effects/session.effects';
import { environment } from '../environments/environment';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

@NgModule({
declarations: [AppComponent, HomeComponent, NavigationBarComponent],
Expand Down Expand Up @@ -78,6 +80,11 @@ import { SessionEffects } from '@app/effects/session.effects';
},
defaultLanguage: 'en'
}),
StoreDevtoolsModule.instrument({
maxAge: 25,

logOnly: environment.production
}),
MatButtonModule,
MatFormFieldModule,
MatTooltipModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</mat-toolbar>

<mat-menu #menu="matMenu">
<button *ngIf="!isAdmin"
<button *ngIf="isAdmin"
mat-menu-item
[matMenuTriggerFor]="adminMenu">
<mat-icon>admin_panel_settings</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import { ConfirmationService } from '@app/core';
import { TranslateService } from '@ngx-translate/core';
import { logoutUser } from '@app/user/actions/user.actions';
import { Store } from '@ngrx/store';
import { ROLE_NAME_ADMIN } from '@app/user/user.constants';
import { isAdmin } from '@app/user/user.functions';

@Component({
selector: 'cx-navigation-bar',
templateUrl: './navigation-bar.component.html',
styleUrls: ['./navigation-bar.component.scss'],
styleUrls: ['./navigation-bar.component.scss']
})
export class NavigationBarComponent {
private _user: User;
Expand All @@ -47,8 +47,7 @@ export class NavigationBarComponent {
@Input()
set user(user: User) {
this._user = user;
this.isAdmin =
!!user && user.roles.some((role) => role.name === ROLE_NAME_ADMIN);
this.isAdmin = isAdmin(user);
}

get user(): User {
Expand All @@ -71,7 +70,7 @@ export class NavigationBarComponent {
this.logger.debug('User logged out');
this.store.dispatch(logoutUser());
this.router.navigate(['login']);
},
}
});
}
}
147 changes: 147 additions & 0 deletions comixed-web/src/app/user/guards/admin.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

import { TestBed } from '@angular/core/testing';
import { AdminGuard } from './admin.guard';
import {
initialState as initialUserState,
USER_FEATURE_KEY
} from '@app/user/reducers/user.reducer';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { LoggerModule } from '@angular-ru/logger';
import { USER_ADMIN, USER_READER } from '@app/user/user.fixtures';
import { Observable } from 'rxjs';

describe('AdminGuard', () => {
const initialState = {
[USER_FEATURE_KEY]: initialUserState
};

let guard: AdminGuard;
let store: MockStore<any>;
let router: Router;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([{ path: '*', redirectTo: '' }]),
LoggerModule.forRoot()
],
providers: [provideMockStore({ initialState })]
});
guard = TestBed.inject(AdminGuard);
store = TestBed.inject(MockStore);
router = TestBed.inject(Router);
spyOn(router, 'navigateByUrl');
});

it('should be created', () => {
expect(guard).toBeTruthy();
});

describe('when the authentication system is not initialized', () => {
beforeEach(() => {
store.setState({
...initialState,
[USER_FEATURE_KEY]: {
...initialUserState,
authenticated: false,
user: null
}
});
});

it('defers access', () => {
(guard.canActivate(null, null) as Observable<
boolean
>).subscribe(response => expect(response).toBeTruthy());
});

afterEach(() => {
store.setState({
...initialState,
[USER_FEATURE_KEY]: {
...initialUserState,
authenticated: true,
user: USER_ADMIN
}
});
});
});

describe('when the user is not authenticated', () => {
beforeEach(() => {
store.setState({
...initialState,
[USER_FEATURE_KEY]: {
...initialUserState,
authenticated: true,
user: null
}
});
});

it('redirects the browser to the login form', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
});

it('denies access', () => {
expect(guard.canActivate(null, null)).toBeFalsy();
});
});

describe('when the user is not an admin', () => {
beforeEach(() => {
store.setState({
...initialState,
[USER_FEATURE_KEY]: {
...initialUserState,
authenticated: true,
user: USER_READER
}
});
});

it('redirects the browser to the root page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/');
});

it('denies access', () => {
expect(guard.canActivate(null, null)).toBeFalsy();
});
});

describe('when the user is an admin', () => {
beforeEach(() => {
store.setState({
...initialState,
[USER_FEATURE_KEY]: {
...initialUserState,
authenticated: true,
user: USER_ADMIN
}
});
});

it('allows access', () => {
expect(guard.canActivate(null, null)).toBeTruthy();
});
});
});
78 changes: 78 additions & 0 deletions comixed-web/src/app/user/guards/admin.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot
} from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { LoggerService } from '@angular-ru/logger';
import { Store } from '@ngrx/store';
import { selectUserState } from '@app/user/selectors/user.selectors';
import { filter } from 'rxjs/operators';
import { isAdmin } from '@app/user/user.functions';

@Injectable({
providedIn: 'root'
})
export class AdminGuard implements CanActivate {
authenticated = false;
isAdmin = false;
delayed$ = new Subject<boolean>();

constructor(
private logger: LoggerService,
private store: Store<any>,
private router: Router
) {
this.store
.select(selectUserState)
.pipe(filter(state => !!state))
.subscribe(state => {
this.logger.debug('Guard: user state updated:', state);
this.authenticated = state.authenticated;
const user = state.user;
if (this.authenticated) {
this.isAdmin = false;
if (!user) {
this.router.navigateByUrl('/login');
} else {
this.isAdmin = isAdmin(user);
this.delayed$.next(this.isAdmin);
if (!this.isAdmin) {
this.router.navigateByUrl('/');
}
}
}
});
}

canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | boolean {
if (this.authenticated) {
return this.isAdmin;
} else {
return this.delayed$.asObservable();
}
}
}

0 comments on commit 00bb7c2

Please sign in to comment.