diff options
| author | crupest <crupest@outlook.com> | 2019-03-06 21:29:36 +0800 | 
|---|---|---|
| committer | crupest <crupest@outlook.com> | 2019-03-06 21:29:36 +0800 | 
| commit | 61844a348b2934321567b1457e6d05f318fc8b7e (patch) | |
| tree | afc70011a886cc325e7d742c05aba2a9982bcf5d /Timeline/ClientApp/src/app/user | |
| parent | 1cf7cad30778e12fa0be22bf3b2acf5f75c6246a (diff) | |
| download | timeline-61844a348b2934321567b1457e6d05f318fc8b7e.tar.gz timeline-61844a348b2934321567b1457e6d05f318fc8b7e.tar.bz2 timeline-61844a348b2934321567b1457e6d05f318fc8b7e.zip  | |
Reorganize file structure.
Diffstat (limited to 'Timeline/ClientApp/src/app/user')
15 files changed, 368 insertions, 0 deletions
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 new file mode 100644 index 00000000..a443e3c0 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css @@ -0,0 +1,5 @@ +.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 new file mode 100644 index 00000000..50d6ba56 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html @@ -0,0 +1,5 @@ +<div [ngSwitch]="state" class="container"> +  <mat-progress-spinner *ngSwitchCase="'loading'" mode="indeterminate" diameter="50"></mat-progress-spinner> +  <app-user-login *ngSwitchCase="'login'" (login)="login($event)" [message]="loginMessage"></app-user-login> +  <app-user-login-success *ngSwitchCase="'success'" [userInfo]="userInfo" [displayLoginSuccessMessage]="displayLoginSuccessMessage"></app-user-login-success> +</div> 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 new file mode 100644 index 00000000..884a3710 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserDialogComponent } from './user-dialog.component'; + +xdescribe('UserDialogComponent', () => { +  let component: UserDialogComponent; +  let fixture: ComponentFixture<UserDialogComponent>; + +  beforeEach(async(() => { +    TestBed.configureTestingModule({ +      declarations: [ UserDialogComponent ] +    }) +    .compileComponents(); +  })); + +  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-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts new file mode 100644 index 00000000..7511de16 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit } from '@angular/core'; +import { UserInfo } from '../user-info'; +import { UserService } from '../user-service/user.service'; +import { LoginEvent, LoginMessage } from '../user-login/user-login.component'; + +@Component({ +  selector: 'app-user-dialog', +  templateUrl: './user-dialog.component.html', +  styleUrls: ['./user-dialog.component.css'] +}) +export class UserDialogComponent implements OnInit { + +  constructor(private userService: UserService) { } + +  state: 'loading' | 'login' | 'success' = 'loading'; + +  loginMessage: LoginMessage; + +  displayLoginSuccessMessage = false; +  userInfo: UserInfo; + +  ngOnInit() { +    this.userService.validateUserLoginState().subscribe(result => { +      if (result.state === 'success') { +        this.userInfo = result.userInfo; +        this.state = 'success'; +      } else { +        this.loginMessage = result.state; +        this.state = 'login'; +      } +    }); +  } + +  login(event: LoginEvent) { +    this.userService.tryLogin(event.username, event.password).subscribe(result => { +      this.userInfo = result; +      this.displayLoginSuccessMessage = true; +      this.state = 'success'; +    }, (error: Error) => { +      this.loginMessage = error.message; +    }); +  } +} diff --git a/Timeline/ClientApp/src/app/user/user-info.ts b/Timeline/ClientApp/src/app/user/user-info.ts new file mode 100644 index 00000000..490b00ba --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-info.ts @@ -0,0 +1,4 @@ +export interface UserInfo { +  username: string; +  roles: string[]; +} diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css new file mode 100644 index 00000000..6486142b --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css @@ -0,0 +1,7 @@ +.login-success-message { +  color: green; +} + +.username { +  color: blue; +} diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html new file mode 100644 index 00000000..943c137f --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html @@ -0,0 +1,5 @@ +<p *ngIf="displayLoginSuccessMessage" class="mat-body login-success-message"> +  Login succeeds! +</p> +<p class="mat-body">You have been login as <span class="username">{{ userInfo.username }}</span>.</p> +<p class="mat-body">Your roles are {{ userInfo.roles.join(', ') }}.</p> 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 new file mode 100644 index 00000000..bdcd354b --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginSuccessComponent } from './user-login-success.component'; + +describe('UserLoginSuccessComponent', () => { +  let component: UserLoginSuccessComponent; +  let fixture: ComponentFixture<UserLoginSuccessComponent>; + +  beforeEach(async(() => { +    TestBed.configureTestingModule({ +      declarations: [ UserLoginSuccessComponent ] +    }) +    .compileComponents(); +  })); + +  beforeEach(() => { +    fixture = TestBed.createComponent(UserLoginSuccessComponent); +    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.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts new file mode 100644 index 00000000..99de5970 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { UserInfo } from '../user-info'; + +@Component({ +  selector: 'app-user-login-success', +  templateUrl: './user-login-success.component.html', +  styleUrls: ['./user-login-success.component.css'] +}) +export class UserLoginSuccessComponent implements OnInit { + +  @Input() +  displayLoginSuccessMessage = false; + +  @Input() +  userInfo: UserInfo; + +  constructor() { } + +  ngOnInit() { +  } + +} diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.css b/Timeline/ClientApp/src/app/user/user-login/user-login.component.css new file mode 100644 index 00000000..8bf6b408 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.css @@ -0,0 +1,24 @@ +form { +  display: flex; +  flex-wrap: wrap; +} + +div.w-100 { +  width: 100%; +} + +.login-button { +  margin-left: auto; +} + +.no-login-message { +  color: blue; +} + +.invalid-login-message { +  color: red; +} + +.error-message { +  color: red; +} diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.html b/Timeline/ClientApp/src/app/user/user-login/user-login.component.html new file mode 100644 index 00000000..b1dd289d --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.html @@ -0,0 +1,18 @@ +<form [formGroup]="form"> +  <ng-container *ngIf="message" [ngSwitch]="message"> +    <p *ngSwitchCase="'nologin'" class="mat-body no-login-message">You haven't login.</p> +    <p *ngSwitchCase="'invalidlogin'" class="mat-body invalid-login-message">Your login is no longer valid.</p> +    <p *ngSwitchDefault class="mat-body error-message">{{ message }}</p> +  </ng-container> +  <mat-form-field> +    <mat-label>Username</mat-label> +    <input formControlName="username" matInput type="text" /> +  </mat-form-field> +  <div class="w-100"></div> +  <mat-form-field> +    <mat-label>Password</mat-label> +    <input formControlName="password" matInput type="password" /> +  </mat-form-field> +  <div class="w-100"></div> +  <button mat-flat-button class="login-button" (appDebounceClick)="onLoginButtonClick()">Login</button> +</form> 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 new file mode 100644 index 00000000..b606b7b4 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginComponent } from './user-login.component'; + +describe('UserLoginComponent', () => { +  let component: UserLoginComponent; +  let fixture: ComponentFixture<UserLoginComponent>; + +  beforeEach(async(() => { +    TestBed.configureTestingModule({ +      declarations: [ UserLoginComponent ] +    }) +    .compileComponents(); +  })); + +  beforeEach(() => { +    fixture = TestBed.createComponent(UserLoginComponent); +    component = fixture.componentInstance; +    fixture.detectChanges(); +  }); + +  it('should create', () => { +    expect(component).toBeTruthy(); +  }); +}); 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 new file mode 100644 index 00000000..da642cb8 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts @@ -0,0 +1,32 @@ +import { Component, Output, OnInit, EventEmitter, Input } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; + +export type LoginMessage = 'nologin' | 'invalidlogin' | string; + +export class LoginEvent { +  username: string; +  password: string; +} + +@Component({ +  selector: 'app-user-login', +  templateUrl: './user-login.component.html', +  styleUrls: ['./user-login.component.css'] +}) +export class UserLoginComponent { + +  @Input() +  message: LoginMessage; + +  @Output() +  login = new EventEmitter<LoginEvent>(); + +  form = new FormGroup({ +    username: new FormControl(''), +    password: new FormControl('') +  }); + +  onLoginButtonClick() { +    this.login.emit(this.form.value); +  } +} diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts new file mode 100644 index 00000000..b9221b90 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +xdescribe('UserService', () => { +  beforeEach(() => TestBed.configureTestingModule({})); + +  it('should be created', () => { +    const service: UserService = TestBed.get(UserService); +    expect(service).toBeTruthy(); +  }); +}); diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.ts new file mode 100644 index 00000000..009e5292 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.ts @@ -0,0 +1,116 @@ +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 { UserInfo } from '../user-info'; + +export interface UserCredentials { +  username: string; +  password: string; +} + +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; +  private userInfo: UserInfo; + +  constructor(private httpClient: HttpClient) { } + +  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(username: string, password: string): Observable<UserInfo> { +    if (this.token) { +      return throwError(new AlreadyLoginException()); +    } + +    return this.httpClient.post<CreateTokenResult>('/api/User/CreateToken', <UserCredentials>{ +      username, password +    }).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; +      }) +    ); +  } +}  | 
