aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src/app/user
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline/ClientApp/src/app/user')
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css5
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html5
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts25
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts43
-rw-r--r--Timeline/ClientApp/src/app/user/user-info.ts4
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css7
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html5
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts25
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts22
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.css24
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.html18
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts25
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.ts32
-rw-r--r--Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts12
-rw-r--r--Timeline/ClientApp/src/app/user/user-service/user.service.ts116
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;
+ })
+ );
+ }
+}