diff options
6 files changed, 66 insertions, 39 deletions
diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json index f725a0f5..7c6d28f0 100644 --- a/Timeline/ClientApp/package.json +++ b/Timeline/ClientApp/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve", - "start-dotnet": "dotnet run --project ..", + "start-dotnet": "dotnet run --project ../Timeline.csproj", "build": "ng build", "build:ssr": "ng run Timeline:server:dev", "test": "ng test", diff --git a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts index 1743e615..72707c5e 100644 --- a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts +++ b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts @@ -1,11 +1,15 @@ -import { ParamMap } from '@angular/router'; +import { ParamMap, ActivatedRouteSnapshot, ActivatedRoute } from '@angular/router'; import { Observable, BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; +export type PartialMock<T> = { + [P in keyof T]?: T[P] | PartialMock<T[P]>; +}; + export interface ParamMapCreator { [name: string]: string | string[]; } -export class MockActivatedRouteSnapshot { +export class MockActivatedRouteSnapshot implements PartialMock<ActivatedRouteSnapshot> { private paramMapInternal: ParamMap; @@ -44,7 +48,7 @@ export class MockActivatedRouteSnapshot { } } -export class MockActivatedRoute { +export class MockActivatedRoute implements PartialMock<ActivatedRoute> { snapshot$ = new BehaviorSubject<MockActivatedRouteSnapshot>(new MockActivatedRouteSnapshot()); diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts index 4767bd16..604393f4 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts @@ -2,10 +2,8 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Router } from '@angular/router'; -import { Observable, of, throwError, BehaviorSubject } from 'rxjs'; -import { map, catchError, retry } from 'rxjs/operators'; - -import { nullIfUndefined } from '../../utilities/language-untilities'; +import { Observable, throwError, BehaviorSubject, of } from 'rxjs'; +import { map, catchError, retry, switchMap, tap } from 'rxjs/operators'; import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors'; import { @@ -13,10 +11,9 @@ import { CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse } from './http-entities'; import { UserCredentials, UserInfo } from '../entities'; +import { MatSnackBar } from '@angular/material'; -export type UserLoginState = 'nologin' | 'invalidlogin' | 'success'; - /** * This service is only used internal in user module. */ @@ -36,41 +33,60 @@ export class InternalUserService { return this.userInfoSubject; } - constructor(private httpClient: HttpClient, private router: Router) { } + constructor(private httpClient: HttpClient, private router: Router, private snackBar: MatSnackBar) { + const savedToken = window.localStorage.getItem('token'); + if (savedToken === null) { + setTimeout(() => snackBar.open('No login before!', 'ok', { duration: 2000 }), 0); + } else { + this.validateToken(savedToken).subscribe(result => { + if (result === null) { + window.localStorage.removeItem('token'); + setTimeout(() => snackBar.open('Last login is no longer invalid!', 'ok', { duration: 2000 }), 0); + } else { + this.token = savedToken; + this.userInfoSubject.next(result); + setTimeout(() => snackBar.open('You have login already!', 'ok', { duration: 2000 }), 0); + } + }, _ => { + setTimeout(() => snackBar.open('Failed to check last login', 'ok', { duration: 2000 }), 0); + }); + } - userRouteNavigate(commands: any[] | null) { - this.router.navigate([{ - outlets: { - user: commands - } - }]); } - refreshAndGetUserState(): Observable<UserLoginState> { - if (this.token === undefined || this.token === null) { - return of(<UserLoginState>'nologin'); - } - - return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ token: this.token }).pipe( + private validateToken(token: string): Observable<UserInfo | null> { + return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ token: token }).pipe( retry(3), - catchError(error => { - console.error('Failed to validate token.'); - return throwError(error); - }), - map(result => { + switchMap(result => { if (result.isValid) { - this.userInfoSubject.next(nullIfUndefined(result.userInfo)); - return <UserLoginState>'success'; + const { userInfo } = result; + if (userInfo) { + return of(userInfo); + } else { + return throwError(new Error('Wrong server response. IsValid is true but UserInfo is null.')); + } } else { - this.token = null; - this.userInfoSubject.next(null); - return <UserLoginState>'invalidlogin'; + return of(null); } - }) + }), + tap({ + error: error => { + console.error('Failed to validate token.'); + console.error(error); + } + }), ); } - tryLogin(credentials: UserCredentials): Observable<UserInfo> { + userRouteNavigate(commands: any[] | null) { + this.router.navigate([{ + outlets: { + user: commands + } + }]); + } + + tryLogin(credentials: UserCredentials, options: { remember: boolean } = { remember: true }): Observable<UserInfo> { if (this.token) { return throwError(new AlreadyLoginError()); } @@ -90,6 +106,9 @@ export class InternalUserService { }), map(result => { this.token = result.token; + if (options.remember) { + window.localStorage.setItem('token', result.token); + } this.userInfoSubject.next(result.userInfo); return result.userInfo; }) diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts index 8f3b9a9c..7645d61d 100644 --- a/Timeline/ClientApp/src/app/user/user.module.ts +++ b/Timeline/ClientApp/src/app/user/user.module.ts @@ -6,7 +6,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; import { MatFormFieldModule, MatProgressSpinnerModule, - MatDialogModule, MatInputModule, MatButtonModule + MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule } from '@angular/material'; import { RequireNoLoginGuard, RequireLoginGuard } from './auth.guard'; @@ -25,7 +25,7 @@ import { UtilityModule } from '../utilities/utility.module'; { path: '**', component: RedirectComponent, outlet: 'user' } ]), CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule, - MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, + MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule, UtilityModule ], exports: [RouterModule], diff --git a/Timeline/ClientApp/src/app/utilities/language-untilities.ts b/Timeline/ClientApp/src/app/utilities/language-untilities.ts index be9df2dc..7f38f3e4 100644 --- a/Timeline/ClientApp/src/app/utilities/language-untilities.ts +++ b/Timeline/ClientApp/src/app/utilities/language-untilities.ts @@ -3,9 +3,9 @@ export function nullIfUndefined<T>(value: T | undefined): T | null { } export function throwIfNullOrUndefined<T>(value: T | null | undefined, - lazyMessage: () => string = () => 'Value mustn\'t be falsy'): T | never { + message: string | (() => string) = 'Value mustn\'t be null or undefined'): T | never { if (value === null || value === undefined) { - throw new Error(lazyMessage()); + throw new Error(typeof message === 'string' ? message : message()); } else { return value; } diff --git a/Timeline/ClientApp/tsconfig.json b/Timeline/ClientApp/tsconfig.json index 437067d6..86c42495 100644 --- a/Timeline/ClientApp/tsconfig.json +++ b/Timeline/ClientApp/tsconfig.json @@ -17,5 +17,9 @@ "dom" ], "strict": true + }, + "angularCompilerOptions": { + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true } } |