diff options
author | crupest <crupest@outlook.com> | 2019-03-09 21:36:55 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2019-03-09 21:36:55 +0800 |
commit | 14a799cc17f16ab93ee652bd9a2973c60cb3697c (patch) | |
tree | 9c34cb2354dd7fb90994c3659ae592f6b616c898 /Timeline/ClientApp/src | |
parent | f65ac5d592e4e449dd513ad01cfd2b980324f240 (diff) | |
download | timeline-14a799cc17f16ab93ee652bd9a2973c60cb3697c.tar.gz timeline-14a799cc17f16ab93ee652bd9a2973c60cb3697c.tar.bz2 timeline-14a799cc17f16ab93ee652bd9a2973c60cb3697c.zip |
Seperate internal and public user service.
Diffstat (limited to 'Timeline/ClientApp/src')
-rw-r--r-- | Timeline/ClientApp/src/app/app.component.ts | 9 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/internal-user-service/errors.ts | 25 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts | 17 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts (renamed from Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts) | 20 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts | 93 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts | 22 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts | 10 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts | 9 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/user-login/user-login.component.ts | 7 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/user-service/user.service.ts | 118 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/user.service.ts | 33 |
11 files changed, 206 insertions, 157 deletions
diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts index 0e2a9799..ee02f833 100644 --- a/Timeline/ClientApp/src/app/app.component.ts +++ b/Timeline/ClientApp/src/app/app.component.ts @@ -1,6 +1,5 @@ import { Component } from '@angular/core'; -import { MatDialog } from '@angular/material'; -import { UserDialogComponent } from './user/user-dialog/user-dialog.component'; +import { UserService } from './user/user.service'; @Component({ selector: 'app-root', @@ -9,11 +8,9 @@ import { UserDialogComponent } from './user/user-dialog/user-dialog.component'; }) export class AppComponent { - constructor(private dialog: MatDialog) { } + constructor(private userService: UserService) { } openUserDialog() { - this.dialog.open(UserDialogComponent, { - width: '300px' - }); + this.userService.openUserDialog(); } } diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts new file mode 100644 index 00000000..22e44dd6 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts @@ -0,0 +1,25 @@ +export abstract class LoginError extends Error { } + +export class BadNetworkError extends LoginError { + constructor() { + super('Network is bad.'); + } +} + +export class AlreadyLoginError extends LoginError { + constructor() { + super('Internal logical error. There is already a token saved. Please call validateUserLoginState first.'); + } +} + +export class BadCredentialsError extends LoginError { + constructor() { + super('Username or password is wrong.'); + } +} + +export class UnknownError extends LoginError { + constructor(public internalError?: any) { + super('Sorry, unknown error occured!'); + } +} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts new file mode 100644 index 00000000..1335b407 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts @@ -0,0 +1,17 @@ +import { UserCredentials, UserInfo } from '../entities'; + +export type CreateTokenRequest = UserCredentials; + +export interface CreateTokenResponse { + token: string; + userInfo: UserInfo; +} + +export interface ValidateTokenRequest { + token: string; +} + +export interface ValidateTokenResponse { + isValid: boolean; + userInfo?: UserInfo; +} diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts index 9effe000..4a2c78f8 100644 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts @@ -4,9 +4,9 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { UserInfo, UserCredentials } from '../entities'; import { - UserService, CreateTokenResult, + InternalUserService, CreateTokenResult, UserLoginState, TokenValidationRequest, TokenValidationResult -} from './user.service'; +} from './internal-user.service'; describe('UserService', () => { const tokenCreateUrl = '/api/User/CreateToken'; @@ -21,19 +21,19 @@ describe('UserService', () => { })); it('should be created', () => { - const service: UserService = TestBed.get(UserService); + const service: InternalUserService = TestBed.get(InternalUserService); expect(service).toBeTruthy(); }); it('should be nologin at first', () => { - const service: UserService = TestBed.get(UserService); - service.validateUserLoginState().subscribe(result => { + const service: InternalUserService = TestBed.get(InternalUserService); + service.refreshAndGetUserState().subscribe(result => { expect(result.state).toBe('nologin'); }); }); it('login should work well', () => { - const service: UserService = TestBed.get(UserService); + const service: InternalUserService = TestBed.get(InternalUserService); const mockUserInfo: UserInfo = { username: 'user', @@ -58,7 +58,7 @@ describe('UserService', () => { }); describe('validateUserLoginState', () => { - let service: UserService; + let service: InternalUserService; let httpController: HttpTestingController; const mockUserInfo: UserInfo = { @@ -73,7 +73,7 @@ describe('UserService', () => { }; beforeEach(() => { - service = TestBed.get(UserService); + service = TestBed.get(InternalUserService); httpController = TestBed.get(HttpTestingController); service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login @@ -85,7 +85,7 @@ describe('UserService', () => { }); it('success should work well', () => { - service.validateUserLoginState().subscribe((result: UserLoginState) => { + service.refreshAndGetUserState().subscribe((result: UserLoginState) => { expect(result).toEqual(<UserLoginState>{ state: 'success', userInfo: mockUserInfo @@ -101,7 +101,7 @@ describe('UserService', () => { }); it('invalid should work well', () => { - service.validateUserLoginState().subscribe((result: UserLoginState) => { + service.refreshAndGetUserState().subscribe((result: UserLoginState) => { expect(result).toEqual(<UserLoginState>{ state: 'invalidlogin' }); 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 new file mode 100644 index 00000000..f6987d7d --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts @@ -0,0 +1,93 @@ +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 { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors'; +import { CreateTokenRequest, CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse } from './http-entities'; +import { UserCredentials, UserInfo } from '../entities'; + + +export type UserLoginState = 'nologin' | 'invalidlogin' | 'success'; + +/** + * This service is only used internal in user module. + */ +@Injectable({ + providedIn: 'root' +}) +export class InternalUserService { + + private token: string; + private userInfoSubject = new BehaviorSubject<UserInfo | null>(null); + + get currentUserInfo(): UserInfo | null { + return this.userInfoSubject.value; + } + + get userInfo$(): Observable<UserInfo | null> { + return this.userInfoSubject; + } + + constructor(private httpClient: HttpClient, private router: Router) { } + + userRouteNavigate(commands: any[]) { + 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>('/api/User/ValidateToken', <ValidateTokenRequest>{ token: this.token }).pipe( + retry(3), + catchError(error => { + console.error('Failed to validate token.'); + return throwError(error); + }), + map(result => { + if (result.isValid) { + this.userInfoSubject.next(result.userInfo); + return <UserLoginState>'success'; + } else { + this.token = null; + this.userInfoSubject.next(null); + return <UserLoginState>'invalidlogin'; + } + }) + ); + } + + tryLogin(credentials: UserCredentials): Observable<UserInfo> { + if (this.token) { + return throwError(new AlreadyLoginError()); + } + + return this.httpClient.post<CreateTokenResponse>('/api/User/CreateToken', <CreateTokenRequest>credentials).pipe( + catchError((error: HttpErrorResponse) => { + if (error.error instanceof ErrorEvent) { + console.error('An error occurred when login: ' + error.error.message); + return throwError(new BadNetworkError()); + } else if (error.status === 400) { + console.error('An error occurred when login: wrong credentials.'); + return throwError(new BadCredentialsError()); + } else { + console.error('An unknown error occurred when login: ' + error); + return throwError(new UnknownError(error)); + } + }), + map(result => { + this.token = result.token; + this.userInfoSubject.next(result.userInfo); + return result.userInfo; + }) + ); + } +} 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 d24c0cd2..dd7af6ca 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 @@ -4,9 +4,9 @@ import { By } from '@angular/platform-browser'; import { of } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { UserInfo } from '../user-info'; +import { UserInfo } from '../entities'; import { UserDialogComponent } from './user-dialog.component'; -import { UserService, UserLoginState } from '../user-service/user.service'; +import { InternalUserService, UserLoginState } from '../internal-user-service/internal-user.service'; import { LoginEvent } from '../user-login/user-login.component'; @Component({ @@ -38,7 +38,7 @@ class UserLoginSuccessStubComponent { } describe('UserDialogComponent', () => { let component: UserDialogComponent; let fixture: ComponentFixture<UserDialogComponent>; - let mockUserService: jasmine.SpyObj<UserService>; + let mockUserService: jasmine.SpyObj<InternalUserService>; beforeEach(async(() => { mockUserService = jasmine.createSpyObj('UserService', ['validateUserLoginState', 'tryLogin']); @@ -46,7 +46,7 @@ describe('UserDialogComponent', () => { TestBed.configureTestingModule({ declarations: [UserDialogComponent, MatProgressSpinnerStubComponent, UserLoginStubComponent, UserLoginSuccessStubComponent], - providers: [{ provide: UserService, useValue: mockUserService }] + providers: [{ provide: InternalUserService, useValue: mockUserService }] }) .compileComponents(); })); @@ -57,7 +57,7 @@ describe('UserDialogComponent', () => { }); it('progress spinner should work well', fakeAsync(() => { - mockUserService.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'nologin' }).pipe(delay(10))); + mockUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>{ state: 'nologin' }).pipe(delay(10))); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('mat-progress-spinner'))).toBeTruthy(); tick(10); @@ -66,30 +66,30 @@ describe('UserDialogComponent', () => { })); it('nologin should work well', () => { - mockUserService.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'nologin' })); + mockUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>{ state: 'nologin' })); fixture.detectChanges(); - expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); }); it('success should work well', () => { - mockUserService.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'success', userInfo: {} })); + mockUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>{ state: 'success', userInfo: {} })); fixture.detectChanges(); - expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('app-user-login'))).toBeFalsy(); expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeTruthy(); }); it('login should work well', () => { - mockUserService.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'nologin' })); + mockUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>{ state: 'nologin' })); fixture.detectChanges(); - expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); 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 0edde924..498ffaa1 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,5 @@ import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; -import { UserService } from '../user-service/user.service'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; import { RouterOutlet, Router, ActivationStart } from '@angular/router'; @Component({ @@ -9,7 +9,7 @@ import { RouterOutlet, Router, ActivationStart } from '@angular/router'; }) export class UserDialogComponent implements OnInit, OnDestroy { - constructor(private userService: UserService, private router: Router) { } + constructor(private userService: InternalUserService, private router: Router) { } @ViewChild(RouterOutlet) outlet: RouterOutlet; @@ -24,12 +24,12 @@ export class UserDialogComponent implements OnInit, OnDestroy { }); - this.userService.validateUserLoginState().subscribe(result => { + this.userService.refreshAndGetUserState().subscribe(result => { this.isLoading = false; - if (result.state === 'success') { + if (result === 'success') { this.userService.userRouteNavigate(['success', { reason: 'already' }]); } else { - this.userService.userRouteNavigate(['login', { reason: result.state }]); + this.userService.userRouteNavigate(['login', { reason: result }]); } }); } 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 d141b3b6..48e331d6 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 @@ -1,8 +1,9 @@ import { Component, OnInit, Input } from '@angular/core'; -import { UserInfo } from '../entities'; -import { UserService } from '../user-service/user.service'; import { ActivatedRoute } from '@angular/router'; +import { UserInfo } from '../entities'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; + @Component({ selector: 'app-user-login-success', templateUrl: './user-login-success.component.html', @@ -14,10 +15,10 @@ export class UserLoginSuccessComponent implements OnInit { userInfo: UserInfo; - constructor(private route: ActivatedRoute, private userService: UserService) { } + constructor(private route: ActivatedRoute, private userService: InternalUserService) { } ngOnInit() { - this.userInfo = this.userService.userInfo; + this.userInfo = this.userService.currentUserInfo; this.displayLoginSuccessMessage = this.route.snapshot.paramMap.get('reason') === 'login'; } } 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 971d57ce..082f879c 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,8 +1,9 @@ import { Component, Output, OnInit, EventEmitter } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; -import { UserService } from '../user-service/user.service'; import { ActivatedRoute } from '@angular/router'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; + export type LoginMessage = 'nologin' | 'invalidlogin' | string; export class LoginEvent { @@ -17,9 +18,9 @@ export class LoginEvent { }) export class UserLoginComponent implements OnInit { - constructor(private route: ActivatedRoute, private userService: UserService) { } + constructor(private route: ActivatedRoute, private userService: InternalUserService) { } - message: string; + message: LoginMessage; form = new FormGroup({ username: new FormControl(''), diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.ts deleted file mode 100644 index e535537d..00000000 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Observable, of, throwError } from 'rxjs'; -import { map, catchError, retry } from 'rxjs/operators'; - -import { UserCredentials, UserInfo } from '../entities'; -import { Router } from '@angular/router'; - -export interface CreateTokenResult { - token: string; - userInfo: UserInfo; -} - -export interface TokenValidationRequest { - token: string; -} - -export interface TokenValidationResult { - isValid: boolean; - userInfo?: UserInfo; -} - -export interface UserLoginState { - state: 'nologin' | 'invalidlogin' | 'success'; - userInfo?: UserInfo; -} - -export class BadNetworkException extends Error { - constructor() { - super('Network is bad.'); - } -} - -export class AlreadyLoginException extends Error { - constructor() { - super('There is already a token saved. Please call validateUserLoginState first.'); - } -} - -export class BadCredentialsException extends Error { - constructor() { - super(`Username or password is wrong.`); - } -} - -@Injectable({ - providedIn: 'root' -}) -export class UserService { - - private token: string; - userInfo: UserInfo; - - constructor(private httpClient: HttpClient, private router: Router) { } - - userRouteNavigate(commands: any[]) { - this.router.navigate([{ - outlets: { - user: commands - } - }]); - } - - validateUserLoginState(): Observable<UserLoginState> { - if (this.token === undefined || this.token === null) { - return of(<UserLoginState>{ state: 'nologin' }); - } - - return this.httpClient.post<TokenValidationResult>('/api/User/ValidateToken', <TokenValidationRequest>{ token: this.token }).pipe( - retry(3), - catchError(error => { - console.error('Failed to validate token.'); - return throwError(error); - }), - map(result => { - if (result.isValid) { - this.userInfo = result.userInfo; - return <UserLoginState>{ - state: 'success', - userInfo: result.userInfo - }; - } else { - this.token = null; - this.userInfo = null; - return <UserLoginState>{ - state: 'invalidlogin' - }; - } - }) - ); - } - - tryLogin(credentials: UserCredentials): Observable<UserInfo> { - if (this.token) { - return throwError(new AlreadyLoginException()); - } - - return this.httpClient.post<CreateTokenResult>('/api/User/CreateToken', credentials).pipe( - catchError((error: HttpErrorResponse) => { - if (error.error instanceof ErrorEvent) { - console.error('An error occurred when login: ' + error.error.message); - return throwError(new BadNetworkException()); - } else if (error.status === 400) { - console.error('An error occurred when login: wrong credentials.'); - return throwError(new BadCredentialsException()); - } else { - console.error('An unknown error occurred when login: ' + error); - return throwError(error); - } - }), - map(result => { - this.token = result.token; - this.userInfo = result.userInfo; - return result.userInfo; - }) - ); - } -} diff --git a/Timeline/ClientApp/src/app/user/user.service.ts b/Timeline/ClientApp/src/app/user/user.service.ts new file mode 100644 index 00000000..e876706c --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material'; + +import { Observable } from 'rxjs'; + +import { UserInfo } from './entities'; +import { InternalUserService } from './internal-user-service/internal-user.service'; +import { UserDialogComponent } from './user-dialog/user-dialog.component'; + + +/** + * This service provides public api of user module. + */ +@Injectable({ + providedIn: 'root' +}) +export class UserService { + constructor(private dialog: MatDialog, private internalService: InternalUserService) { } + + get currentUserInfo(): UserInfo | null { + return this.internalService.currentUserInfo; + } + + get userInfo$(): Observable<UserInfo | null> { + return this.internalService.userInfo$; + } + + openUserDialog() { + this.dialog.open(UserDialogComponent, { + width: '300px' + }); + } +} |