From eb6cef70b6f9d1060556592dbf474cf54a174902 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 11 Mar 2019 23:21:44 +0800 Subject: Add auth guard. --- Timeline/ClientApp/src/app/user/auth.guard.spec.ts | 15 ++++++ Timeline/ClientApp/src/app/user/auth.guard.ts | 62 ++++++++++++++++++++++ Timeline/ClientApp/src/app/user/user.module.ts | 9 ++-- 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 Timeline/ClientApp/src/app/user/auth.guard.spec.ts create mode 100644 Timeline/ClientApp/src/app/user/auth.guard.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts new file mode 100644 index 00000000..7ed05ee8 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, async, inject } from '@angular/core/testing'; + +import { AuthGuard } from './auth.guard'; + +describe('AuthGuard', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AuthGuard] + }); + }); + + it('should ...', inject([AuthGuard], (guard: AuthGuard) => { + expect(guard).toBeTruthy(); + })); +}); diff --git a/Timeline/ClientApp/src/app/user/auth.guard.ts b/Timeline/ClientApp/src/app/user/auth.guard.ts new file mode 100644 index 00000000..16f66cd8 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/auth.guard.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { UserService } from './user.service'; + +export type RequiredAuthData = 'all' | 'requirelogin' | 'requirenologin' | string[]; + +export abstract class AuthGuard implements CanActivate { + + constructor(private userService: UserService) { } + + abstract get requiredAuth(): RequiredAuthData; + + canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): + Observable | Promise | boolean | UrlTree { + + const { requiredAuth } = this; + + if (requiredAuth === 'all') { + return true; + } + const { currentUserInfo } = this.userService; + + if (currentUserInfo === null) { + return requiredAuth === 'requirenologin'; + } else { + if (requiredAuth === 'requirelogin') { + return true; + } else if (requiredAuth === 'requirenologin') { + return false; + } else { + const { roles } = currentUserInfo; + return requiredAuth.every(value => roles.includes(value)); + } + } + } +} + +@Injectable({ + providedIn: 'root' +}) +export class RequireLoginGuard extends AuthGuard { + readonly requiredAuth: RequiredAuthData = 'requirelogin'; + + // never remove this constructor or you will get an injection error. + constructor(userService: UserService) { + super(userService); + } +} + +@Injectable({ + providedIn: 'root' +}) +export class RequireNoLoginGuard extends AuthGuard { + readonly requiredAuth: RequiredAuthData = 'requirenologin'; + + // never remove this constructor or you will get an injection error. + constructor(userService: UserService) { + super(userService); + } +} diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts index c399c9e0..ae842f78 100644 --- a/Timeline/ClientApp/src/app/user/user.module.ts +++ b/Timeline/ClientApp/src/app/user/user.module.ts @@ -2,24 +2,25 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule } from '@angular/router'; import { MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule } from '@angular/material'; +import { RequireNoLoginGuard, RequireLoginGuard } from './auth.guard'; import { UserDialogComponent } from './user-dialog/user-dialog.component'; import { UserLoginComponent } from './user-login/user-login.component'; import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; import { UtilityModule } from '../utilities/utility.module'; -import { RouterModule } from '@angular/router'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent], imports: [ RouterModule.forChild([ - { path: 'login', component: UserLoginComponent, outlet: 'user' }, - { path: 'success', component: UserLoginSuccessComponent, outlet: 'user' } + { path: 'login', canActivate: [RequireNoLoginGuard], component: UserLoginComponent, outlet: 'user' }, + { path: 'success', canActivate: [RequireLoginGuard], component: UserLoginSuccessComponent, outlet: 'user' } ]), CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule, MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, -- cgit v1.2.3 From 5bfbb5020904eadba298fdc094172d1c12879278 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 13 Mar 2019 22:04:09 +0800 Subject: Use route to control user dialog. --- Timeline/ClientApp/src/app/app.component.html | 6 +-- Timeline/ClientApp/src/app/app.component.ts | 9 ++-- Timeline/ClientApp/src/app/app.module.ts | 2 + Timeline/ClientApp/src/app/user/auth.guard.ts | 59 ++++++++++++++-------- .../app/user/user-dialog/user-dialog.component.css | 5 -- .../user/user-dialog/user-dialog.component.html | 5 +- .../user/user-dialog/user-dialog.component.spec.ts | 52 +------------------ .../app/user/user-dialog/user-dialog.component.ts | 23 ++------- Timeline/ClientApp/src/app/user/user.service.ts | 28 ++++++++-- 9 files changed, 76 insertions(+), 113 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/app.component.html b/Timeline/ClientApp/src/app/app.component.html index a5df80ac..92c88625 100644 --- a/Timeline/ClientApp/src/app/app.component.html +++ b/Timeline/ClientApp/src/app/app.component.html @@ -1,12 +1,12 @@ - Timeline + Timeline TodoList - +
diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts index ee02f833..33f33048 100644 --- a/Timeline/ClientApp/src/app/app.component.ts +++ b/Timeline/ClientApp/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { UserService } from './user/user.service'; @Component({ @@ -7,10 +8,6 @@ import { UserService } from './user/user.service'; styleUrls: ['./app.component.css'] }) export class AppComponent { - - constructor(private userService: UserService) { } - - openUserDialog() { - this.userService.openUserDialog(); - } + // never remove userService because we need it explicit constructing. + constructor(userService: UserService) { } } diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts index 85c4c43d..b75e10e2 100644 --- a/Timeline/ClientApp/src/app/app.module.ts +++ b/Timeline/ClientApp/src/app/app.module.ts @@ -9,6 +9,7 @@ import { AppComponent } from './app.component'; import { TodoModule } from './todo/todo.module'; import { HomeModule } from './home/home.module'; import { UserModule } from './user/user.module'; +import { UserService } from './user/user.service'; @NgModule({ @@ -22,6 +23,7 @@ import { UserModule } from './user/user.module'; { path: '', redirectTo: '/home', pathMatch: 'full' }, ]) ], + providers: [UserService], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/Timeline/ClientApp/src/app/user/auth.guard.ts b/Timeline/ClientApp/src/app/user/auth.guard.ts index 16f66cd8..64ff93c7 100644 --- a/Timeline/ClientApp/src/app/user/auth.guard.ts +++ b/Timeline/ClientApp/src/app/user/auth.guard.ts @@ -2,38 +2,47 @@ import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; import { Observable } from 'rxjs'; -import { UserService } from './user.service'; +import { InternalUserService } from './internal-user-service/internal-user.service'; -export type RequiredAuthData = 'all' | 'requirelogin' | 'requirenologin' | string[]; +export type AuthStrategy = 'all' | 'requirelogin' | 'requirenologin' | string[]; export abstract class AuthGuard implements CanActivate { - constructor(private userService: UserService) { } + constructor(protected internalUserService: InternalUserService) { } - abstract get requiredAuth(): RequiredAuthData; + onAuthFailed() { } + + abstract get authStrategy(): AuthStrategy; canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - const { requiredAuth } = this; + const { authStrategy } = this; - if (requiredAuth === 'all') { + if (authStrategy === 'all') { return true; } - const { currentUserInfo } = this.userService; + + const { currentUserInfo } = this.internalUserService; if (currentUserInfo === null) { - return requiredAuth === 'requirenologin'; + if (authStrategy === 'requirenologin') { + return true; + } } else { - if (requiredAuth === 'requirelogin') { + if (authStrategy === 'requirelogin') { return true; - } else if (requiredAuth === 'requirenologin') { - return false; - } else { + } else if (authStrategy instanceof Array) { const { roles } = currentUserInfo; - return requiredAuth.every(value => roles.includes(value)); + if (authStrategy.every(value => roles.includes(value))) { + return true; + } } } + + // reach here means auth fails + this.onAuthFailed(); + return false; } } @@ -41,22 +50,30 @@ export abstract class AuthGuard implements CanActivate { providedIn: 'root' }) export class RequireLoginGuard extends AuthGuard { - readonly requiredAuth: RequiredAuthData = 'requirelogin'; + readonly authStrategy: AuthStrategy = 'requirelogin'; // never remove this constructor or you will get an injection error. - constructor(userService: UserService) { - super(userService); - } + constructor(internalUserService: InternalUserService) { + super(internalUserService); + } + + onAuthFailed() { + this.internalUserService.userRouteNavigate(['login', { reason: 'nologin' }]); + } } @Injectable({ providedIn: 'root' }) export class RequireNoLoginGuard extends AuthGuard { - readonly requiredAuth: RequiredAuthData = 'requirenologin'; + readonly authStrategy: AuthStrategy = 'requirenologin'; // never remove this constructor or you will get an injection error. - constructor(userService: UserService) { - super(userService); - } + constructor(internalUserService: InternalUserService) { + super(internalUserService); + } + + onAuthFailed() { + this.internalUserService.userRouteNavigate(['success']); + } } diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css index a443e3c0..e69de29b 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css @@ -1,5 +0,0 @@ -.container { - display: flex; - justify-content: center; - align-content: center; -} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html index 58dff0e4..e8dbb003 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html @@ -1,4 +1 @@ -
- - -
+ diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts index c56e1ed1..fbabdb1a 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -6,15 +6,6 @@ import { of, Observable } from 'rxjs'; import { delay } from 'rxjs/operators'; import { UserDialogComponent } from './user-dialog.component'; -import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock'; -import { InternalUserService, UserLoginState } from '../internal-user-service/internal-user.service'; - -@Component({ - /* tslint:disable-next-line:component-selector*/ - selector: 'mat-progress-spinner', - template: '' -}) -class MatProgressSpinnerStubComponent { } @Component({ /* tslint:disable-next-line:component-selector*/ @@ -27,16 +18,12 @@ class RouterOutletStubComponent { } describe('UserDialogComponent', () => { let component: UserDialogComponent; let fixture: ComponentFixture; - let mockInternalUserService: jasmine.SpyObj; beforeEach(async(() => { - mockInternalUserService = createMockInternalUserService(); - TestBed.configureTestingModule({ - declarations: [UserDialogComponent, MatProgressSpinnerStubComponent, RouterOutletStubComponent], - providers: [{ provide: InternalUserService, useValue: mockInternalUserService }, - { // for the workaround + declarations: [UserDialogComponent, RouterOutletStubComponent], + providers: [{ // for the workaround provide: Router, useValue: { events: new Observable() } @@ -50,39 +37,4 @@ describe('UserDialogComponent', () => { component = fixture.componentInstance; }); - it('progress spinner should work well', fakeAsync(() => { - mockInternalUserService.refreshAndGetUserState.and.returnValue(of('nologin').pipe(delay(10))); - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('mat-progress-spinner'))).toBeTruthy(); - tick(10); - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('mat-progress-spinner'))).toBeFalsy(); - })); - - it('nologin should work well', () => { - mockInternalUserService.refreshAndGetUserState.and.returnValue(of('nologin')); - - fixture.detectChanges(); - - expect(mockInternalUserService.refreshAndGetUserState).toHaveBeenCalled(); - expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['login', { reason: 'nologin' }]); - }); - - it('invalid login should work well', () => { - mockInternalUserService.refreshAndGetUserState.and.returnValue(of('invalidlogin')); - - fixture.detectChanges(); - - expect(mockInternalUserService.refreshAndGetUserState).toHaveBeenCalled(); - expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['login', { reason: 'invalidlogin' }]); - }); - - it('success should work well', () => { - mockInternalUserService.refreshAndGetUserState.and.returnValue(of('success')); - - fixture.detectChanges(); - - expect(mockInternalUserService.refreshAndGetUserState).toHaveBeenCalled(); - expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['success', { reason: 'already' }]); - }); }); diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts index cf5f3643..2887f0a6 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts @@ -1,5 +1,4 @@ -import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; -import { InternalUserService } from '../internal-user-service/internal-user.service'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { RouterOutlet, Router, ActivationStart } from '@angular/router'; @Component({ @@ -7,14 +6,12 @@ import { RouterOutlet, Router, ActivationStart } from '@angular/router'; templateUrl: './user-dialog.component.html', styleUrls: ['./user-dialog.component.css'] }) -export class UserDialogComponent implements OnInit, OnDestroy { +export class UserDialogComponent implements OnInit { - constructor(private userService: InternalUserService, private router: Router) { } + constructor(private router: Router) { } @ViewChild(RouterOutlet) outlet!: RouterOutlet; - isLoading = true; - ngOnInit() { // this is a workaround for a bug. see https://github.com/angular/angular/issues/20694 const subscription = this.router.events.subscribe(e => { @@ -23,19 +20,5 @@ export class UserDialogComponent implements OnInit, OnDestroy { subscription.unsubscribe(); } }); - - - this.userService.refreshAndGetUserState().subscribe(result => { - this.isLoading = false; - if (result === 'success') { - this.userService.userRouteNavigate(['success', { reason: 'already' }]); - } else { - this.userService.userRouteNavigate(['login', { reason: result }]); - } - }); - } - - ngOnDestroy() { - this.userService.userRouteNavigate(null); } } diff --git a/Timeline/ClientApp/src/app/user/user.service.ts b/Timeline/ClientApp/src/app/user/user.service.ts index e876706c..076d0c21 100644 --- a/Timeline/ClientApp/src/app/user/user.service.ts +++ b/Timeline/ClientApp/src/app/user/user.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; -import { MatDialog } from '@angular/material'; +import { MatDialog, MatDialogRef } from '@angular/material'; +import { Router, ActivationStart } from '@angular/router'; import { Observable } from 'rxjs'; @@ -15,7 +16,16 @@ import { UserDialogComponent } from './user-dialog/user-dialog.component'; providedIn: 'root' }) export class UserService { - constructor(private dialog: MatDialog, private internalService: InternalUserService) { } + + private dialogRef: MatDialogRef | null = null; + + constructor(router: Router, private dialog: MatDialog, private internalService: InternalUserService) { + router.events.subscribe(event => { + if (event instanceof ActivationStart && event.snapshot.outlet === 'user') { + setTimeout(() => this.openUserDialog(), 0); + } + }); + } get currentUserInfo(): UserInfo | null { return this.internalService.currentUserInfo; @@ -25,9 +35,19 @@ export class UserService { return this.internalService.userInfo$; } - openUserDialog() { - this.dialog.open(UserDialogComponent, { + private openUserDialog() { + if (this.dialogRef) { + return; + } + + this.dialogRef = this.dialog.open(UserDialogComponent, { width: '300px' }); + + const subscription = this.dialogRef.afterClosed().subscribe(_ => { + this.internalService.userRouteNavigate(null); + this.dialogRef = null; + subscription.unsubscribe(); + }); } } -- cgit v1.2.3 From 5102efa0b2b8a009be1db3bc6fa5462772ef3d6a Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 13 Mar 2019 22:23:35 +0800 Subject: ... --- Timeline/ClientApp/src/app/user/auth.guard.ts | 2 +- .../user/user-login-success/user-login-success.component.ts | 2 +- .../src/app/user/user-login/user-login.component.ts | 12 +++++++----- Timeline/ClientApp/src/styles.css | 4 ++++ 4 files changed, 13 insertions(+), 7 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/auth.guard.ts b/Timeline/ClientApp/src/app/user/auth.guard.ts index 64ff93c7..561a0c53 100644 --- a/Timeline/ClientApp/src/app/user/auth.guard.ts +++ b/Timeline/ClientApp/src/app/user/auth.guard.ts @@ -58,7 +58,7 @@ export class RequireLoginGuard extends AuthGuard { } onAuthFailed() { - this.internalUserService.userRouteNavigate(['login', { reason: 'nologin' }]); + this.internalUserService.userRouteNavigate(['login']); } } diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts index 22f6a41f..2ae584d6 100644 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts @@ -20,6 +20,6 @@ export class UserLoginSuccessComponent implements OnInit { ngOnInit() { this.userInfo = throwIfNullOrUndefined(this.userService.currentUserInfo, () => 'Route error. No login now!'); - this.displayLoginSuccessMessage = this.route.snapshot.paramMap.get('reason') === 'login'; + this.displayLoginSuccessMessage = this.route.snapshot.paramMap.get('fromlogin') === 'true'; } } diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts index 3505d50b..836202de 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts @@ -1,11 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; import { InternalUserService } from '../internal-user-service/internal-user.service'; -export type LoginMessage = 'nologin' | 'invalidlogin' | string | null | undefined; +export type LoginMessage = 'nologin' | 'invalidlogin' | string | null | undefined; @Component({ selector: 'app-user-login', @@ -14,7 +13,7 @@ export type LoginMessage = 'nologin' | 'invalidlogin' | string | null | undefine }) export class UserLoginComponent implements OnInit { - constructor(private route: ActivatedRoute, private userService: InternalUserService) { } + constructor(private userService: InternalUserService) { } message: LoginMessage; @@ -24,12 +23,15 @@ export class UserLoginComponent implements OnInit { }); ngOnInit() { - this.message = this.route.snapshot.paramMap.get('reason'); + if (this.userService.currentUserInfo) { + throw new Error('Route error! Already login!'); + } + this.message = 'nologin'; } onLoginButtonClick() { this.userService.tryLogin(this.form.value).subscribe(_ => { - this.userService.userRouteNavigate(['success', { reason: 'login' }]); + this.userService.userRouteNavigate(['success', { fromlogin: 'true' }]); }, (error: Error) => this.message = error.message); } } diff --git a/Timeline/ClientApp/src/styles.css b/Timeline/ClientApp/src/styles.css index fad44c53..f60c9204 100644 --- a/Timeline/ClientApp/src/styles.css +++ b/Timeline/ClientApp/src/styles.css @@ -1,6 +1,10 @@ /* You can add global styles to this file, and also import other style files */ @import "~@angular/material/prebuilt-themes/indigo-pink.css"; +html { + overflow: unset!important; /* why cdk-global-scrollblock add overflow-y: scroll ??????????? */ +} + body { margin: 0; } -- cgit v1.2.3 From f7b51fa06459493ba2987eb4bb93e1a4430bd4b6 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 13 Mar 2019 22:39:41 +0800 Subject: Add unit test. --- .../user/user-dialog/user-dialog.component.spec.ts | 11 +++++++---- .../user-login-success.component.spec.ts | 4 +--- .../app/user/user-login/user-login.component.spec.ts | 19 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts index fbabdb1a..47860eee 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core'; -import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Router, Event } from '@angular/router'; -import { of, Observable } from 'rxjs'; -import { delay } from 'rxjs/operators'; + +import { Observable } from 'rxjs'; import { UserDialogComponent } from './user-dialog.component'; @@ -35,6 +34,10 @@ describe('UserDialogComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(UserDialogComponent); component = fixture.componentInstance; + fixture.detectChanges(); }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts index 1efbb5c7..ff253add 100644 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts @@ -60,10 +60,8 @@ describe('UserLoginSuccessComponent', () => { }); it('login success message should display well', () => { - mockActivatedRoute.pushSnapshotWithParamMap({ reason: 'login' }); - + mockActivatedRoute.pushSnapshotWithParamMap({ fromlogin: 'true' }); fixture.detectChanges(); - expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeTruthy(); }); }); diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts index 9c9ee1dc..693d5b6e 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts @@ -7,7 +7,6 @@ import { ActivatedRoute } from '@angular/router'; import { of, throwError } from 'rxjs'; import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock'; -import { MockActivatedRoute } from '../../test-utilities/activated-route.mock'; import { UserLoginComponent } from './user-login.component'; import { InternalUserService } from '../internal-user-service/internal-user.service'; import { UserInfo } from '../entities'; @@ -16,17 +15,17 @@ describe('UserLoginComponent', () => { let component: UserLoginComponent; let fixture: ComponentFixture; let mockInternalUserService: jasmine.SpyObj; - let mockActivatedRoute: MockActivatedRoute; beforeEach(async(() => { mockInternalUserService = createMockInternalUserService(); - mockActivatedRoute = new MockActivatedRoute(); + + // mock property + (mockInternalUserService).currentUserInfo = null; TestBed.configureTestingModule({ declarations: [UserLoginComponent], providers: [ - { provide: InternalUserService, useValue: mockInternalUserService }, - { provide: ActivatedRoute, useValue: mockActivatedRoute } + { provide: InternalUserService, useValue: mockInternalUserService } ], imports: [ReactiveFormsModule], schemas: [NO_ERRORS_SCHEMA] @@ -77,22 +76,22 @@ describe('UserLoginComponent', () => { component.onLoginButtonClick(); expect(mockInternalUserService.tryLogin).toHaveBeenCalledWith(mockValue); - expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['success', { reason: 'login' }]); + expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['success', { fromlogin: 'true' }]); }); describe('message display', () => { it('nologin reason should display', () => { - mockActivatedRoute.pushSnapshotWithParamMap({ reason: 'nologin' }); fixture.detectChanges(); - expect(component.message).toBe('nologin'); + component.message = 'nologin'; + fixture.detectChanges(); expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as HTMLParagraphElement).textContent).toBe('You haven\'t login.'); }); it('invalid login reason should display', () => { - mockActivatedRoute.pushSnapshotWithParamMap({ reason: 'invalidlogin' }); fixture.detectChanges(); - expect(component.message).toBe('invalidlogin'); + component.message = 'invalidlogin'; + fixture.detectChanges(); expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as HTMLParagraphElement).textContent).toBe('Your login is no longer valid.'); }); -- cgit v1.2.3 From a918e0503fe27dc13d645bc91c51ad18ada9a1d2 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 13 Mar 2019 23:05:50 +0800 Subject: Add wildcard route redirection in user. --- Timeline/ClientApp/src/app/user/redirect.component.ts | 15 +++++++++++++++ Timeline/ClientApp/src/app/user/user.module.ts | 6 ++++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 Timeline/ClientApp/src/app/user/redirect.component.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/redirect.component.ts b/Timeline/ClientApp/src/app/user/redirect.component.ts new file mode 100644 index 00000000..438b38b9 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/redirect.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; +import { InternalUserService } from './internal-user-service/internal-user.service'; + +@Component({ + selector: 'app-redirect', + template: '' +}) +export class RedirectComponent implements OnInit { + + constructor(private userService: InternalUserService) { } + + ngOnInit() { + this.userService.userRouteNavigate(['login']); + } +} diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts index ae842f78..8f3b9a9c 100644 --- a/Timeline/ClientApp/src/app/user/user.module.ts +++ b/Timeline/ClientApp/src/app/user/user.module.ts @@ -13,14 +13,16 @@ import { RequireNoLoginGuard, RequireLoginGuard } from './auth.guard'; import { UserDialogComponent } from './user-dialog/user-dialog.component'; import { UserLoginComponent } from './user-login/user-login.component'; import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; +import { RedirectComponent } from './redirect.component'; import { UtilityModule } from '../utilities/utility.module'; @NgModule({ - declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent], + declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent, RedirectComponent], imports: [ RouterModule.forChild([ { path: 'login', canActivate: [RequireNoLoginGuard], component: UserLoginComponent, outlet: 'user' }, - { path: 'success', canActivate: [RequireLoginGuard], component: UserLoginSuccessComponent, outlet: 'user' } + { path: 'success', canActivate: [RequireLoginGuard], component: UserLoginSuccessComponent, outlet: 'user' }, + { path: '**', component: RedirectComponent, outlet: 'user' } ]), CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule, MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, -- cgit v1.2.3 From cdf413f103bfd9f81b75140b3da09230d08c788a Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 14 Mar 2019 16:34:26 +0800 Subject: Add auth guard unit test. --- Timeline/ClientApp/src/app/user/auth.guard.spec.ts | 64 +++++++++++++++++++--- 1 file changed, 55 insertions(+), 9 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts index 7ed05ee8..42e35bf7 100644 --- a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts +++ b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts @@ -1,15 +1,61 @@ -import { TestBed, async, inject } from '@angular/core/testing'; - -import { AuthGuard } from './auth.guard'; +import { AuthGuard, AuthStrategy } from './auth.guard'; +import { UserInfo } from './entities'; describe('AuthGuard', () => { + class ConfiurableAuthGuard extends AuthGuard { + constructor(mockInternalUserService: any) { + super(mockInternalUserService); + } + + authStrategy: AuthStrategy = 'all'; + onAuthFailed: () => void = () => { }; + } + + let mockUserService: { currentUserInfo: UserInfo | null }; + let guard: ConfiurableAuthGuard; + let onAuthFialedSpy: jasmine.Spy; + + const mockRoles = ['role1', 'role2']; + + interface ActivateResultMap { + nologin: boolean; + loginWithNoRole: boolean; + loginWithMockRoles: boolean; + } + + + function createTest(authStrategy: AuthStrategy, result: ActivateResultMap): () => void { + return () => { + guard.authStrategy = authStrategy; + + mockUserService.currentUserInfo = null; + expect(guard.canActivate(null, null)).toBe(result.nologin); + + mockUserService.currentUserInfo = { username: 'user', roles: [] }; + expect(guard.canActivate(null, null)).toBe(result.loginWithNoRole); + + mockUserService.currentUserInfo = { username: 'user', roles: mockRoles }; + expect(guard.canActivate(null, null)).toBe(result.loginWithMockRoles); + }; + } + beforeEach(() => { - TestBed.configureTestingModule({ - providers: [AuthGuard] - }); + mockUserService = { currentUserInfo: null }; + guard = new ConfiurableAuthGuard(mockUserService); + onAuthFialedSpy = spyOn(guard, 'onAuthFailed'); }); - it('should ...', inject([AuthGuard], (guard: AuthGuard) => { - expect(guard).toBeTruthy(); - })); + + it('all should work', createTest('all', { nologin: true, loginWithNoRole: true, loginWithMockRoles: true })); + it('require login should work', createTest('requirelogin', { nologin: false, loginWithNoRole: true, loginWithMockRoles: true })); + it('require no login should work', createTest('requirenologin', { nologin: true, loginWithNoRole: false, loginWithMockRoles: false })); + it('good roles should work', createTest(mockRoles, { nologin: false, loginWithNoRole: false, loginWithMockRoles: true })); + it('bad roles should work', createTest(['role3'], { nologin: false, loginWithNoRole: false, loginWithMockRoles: false })); + + it('auth failed callback should be called', () => { + guard.authStrategy = 'requirelogin'; + mockUserService.currentUserInfo = null; + guard.canActivate(null, null); + expect(onAuthFialedSpy).toHaveBeenCalled(); + }); }); -- cgit v1.2.3 From 780afcc14029d966f74fc8688aa040183ac23476 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 14 Mar 2019 16:38:02 +0800 Subject: Only open user dialog when it is not opened! --- Timeline/ClientApp/src/app/user/user.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/user.service.ts b/Timeline/ClientApp/src/app/user/user.service.ts index 076d0c21..e7d50dd2 100644 --- a/Timeline/ClientApp/src/app/user/user.service.ts +++ b/Timeline/ClientApp/src/app/user/user.service.ts @@ -22,7 +22,9 @@ export class UserService { constructor(router: Router, private dialog: MatDialog, private internalService: InternalUserService) { router.events.subscribe(event => { if (event instanceof ActivationStart && event.snapshot.outlet === 'user') { - setTimeout(() => this.openUserDialog(), 0); + if (!this.dialogRef) { + setTimeout(() => this.openUserDialog(), 0); + } } }); } -- cgit v1.2.3