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/auth.guard.spec.ts69
-rw-r--r--Timeline/ClientApp/src/app/user/auth.guard.ts80
-rw-r--r--Timeline/ClientApp/src/app/user/entities.ts9
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/errors.ts29
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts21
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts5
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts123
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts155
-rw-r--r--Timeline/ClientApp/src/app/user/redirect.component.ts15
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css0
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html1
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts43
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts24
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css22
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html6
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts75
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts25
-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.html19
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts123
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.ts38
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css0
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html1
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts35
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts16
-rw-r--r--Timeline/ClientApp/src/app/user/user.module.ts38
-rw-r--r--Timeline/ClientApp/src/app/user/user.service.ts55
-rw-r--r--Timeline/ClientApp/src/app/user/window-inject-token.ts3
28 files changed, 0 insertions, 1054 deletions
diff --git a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts
deleted file mode 100644
index 6a36fea6..00000000
--- a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Observable, of } from 'rxjs';
-
-import { AuthGuard, AuthStrategy } from './auth.guard';
-import { UserInfo } from './entities';
-
-describe('AuthGuard', () => {
- class ConfiurableAuthGuard extends AuthGuard {
- constructor(mockInternalUserService: any) {
- super(mockInternalUserService);
- }
-
- authStrategy: AuthStrategy = 'all';
- }
-
- let mockUserService: { userInfo$: Observable<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;
-
- function testWith(userInfo: UserInfo | null, r: boolean) {
- mockUserService.userInfo$ = of(userInfo);
-
- const rawResult = guard.canActivate(<any>null, <any>null);
- if (typeof rawResult === 'boolean') {
- expect(rawResult).toBe(r);
- } else if (rawResult instanceof Observable) {
- rawResult.subscribe(next => expect(next).toBe(r));
- } else {
- throw new Error('Unsupported return type.');
- }
- }
-
- testWith(null, result.nologin);
- testWith({ username: 'user', roles: [] }, result.loginWithNoRole);
- testWith({ username: 'user', roles: mockRoles }, result.loginWithMockRoles);
- };
- }
-
- beforeEach(() => {
- mockUserService = { userInfo$: of(null) };
- guard = new ConfiurableAuthGuard(mockUserService);
- onAuthFialedSpy = spyOn(guard, 'onAuthFailed');
- });
-
-
- 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';
- (<Observable<boolean>>guard.canActivate(<any>null, <any>null)).subscribe();
- expect(onAuthFialedSpy).toHaveBeenCalled();
- });
-});
diff --git a/Timeline/ClientApp/src/app/user/auth.guard.ts b/Timeline/ClientApp/src/app/user/auth.guard.ts
deleted file mode 100644
index 1fc7a7c0..00000000
--- a/Timeline/ClientApp/src/app/user/auth.guard.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { Injectable } from '@angular/core';
-import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
-import { Observable } from 'rxjs';
-import { take, map } from 'rxjs/operators';
-
-import { InternalUserService } from './internal-user-service/internal-user.service';
-
-export type AuthStrategy = 'all' | 'requirelogin' | 'requirenologin' | string[];
-
-export abstract class AuthGuard implements CanActivate {
-
- constructor(protected internalUserService: InternalUserService) { }
-
- onAuthFailed() { }
-
- abstract get authStrategy(): AuthStrategy;
-
- canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot):
- Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
-
- const { authStrategy } = this;
-
- if (authStrategy === 'all') {
- return true;
- }
-
- return this.internalUserService.userInfo$.pipe(take(1), map(userInfo => {
- if (userInfo === null) {
- if (authStrategy === 'requirenologin') {
- return true;
- }
- } else {
- if (authStrategy === 'requirelogin') {
- return true;
- } else if (authStrategy instanceof Array) {
- const { roles } = userInfo;
- if (authStrategy.every(value => roles.includes(value))) {
- return true;
- }
- }
- }
-
- // reach here means auth fails
- this.onAuthFailed();
- return false;
- }));
- }
-}
-
-@Injectable({
- providedIn: 'root'
-})
-export class RequireLoginGuard extends AuthGuard {
- readonly authStrategy: AuthStrategy = 'requirelogin';
-
- // never remove this constructor or you will get an injection error.
- constructor(internalUserService: InternalUserService) {
- super(internalUserService);
- }
-
- onAuthFailed() {
- this.internalUserService.userRouteNavigate(['login']);
- }
-}
-
-@Injectable({
- providedIn: 'root'
-})
-export class RequireNoLoginGuard extends AuthGuard {
- readonly authStrategy: AuthStrategy = 'requirenologin';
-
- // never remove this constructor or you will get an injection error.
- constructor(internalUserService: InternalUserService) {
- super(internalUserService);
- }
-
- onAuthFailed() {
- this.internalUserService.userRouteNavigate(['success']);
- }
-}
diff --git a/Timeline/ClientApp/src/app/user/entities.ts b/Timeline/ClientApp/src/app/user/entities.ts
deleted file mode 100644
index 6d432ec6..00000000
--- a/Timeline/ClientApp/src/app/user/entities.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export interface UserCredentials {
- username: string;
- password: string;
-}
-
-export interface UserInfo {
- username: string;
- roles: string[];
-}
diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts
deleted file mode 100644
index 3358a9d9..00000000
--- a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-export class BadNetworkError extends Error {
- constructor() {
- super('Network is bad.');
- }
-}
-
-export class AlreadyLoginError extends Error {
- constructor() {
- super('Internal logical error. There is already a token saved. Please call validateUserLoginState first.');
- }
-}
-
-export class BadCredentialsError extends Error {
- constructor() {
- super('Username or password is wrong.');
- }
-}
-
-export class UnknownError extends Error {
- constructor(public internalError?: any) {
- super('Sorry, unknown error occured!');
- }
-}
-
-export class ServerInternalError extends Error {
- constructor(message?: string) {
- super('Wrong server response. ' + message);
- }
-}
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
deleted file mode 100644
index f52233c9..00000000
--- a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { UserCredentials, UserInfo } from '../entities';
-
-export const createTokenUrl = '/api/User/CreateToken';
-export const validateTokenUrl = '/api/User/ValidateToken';
-
-export type CreateTokenRequest = UserCredentials;
-
-export interface CreateTokenResponse {
- success: boolean;
- token?: string;
- userInfo?: UserInfo;
-}
-
-export interface ValidateTokenRequest {
- token: string;
-}
-
-export interface ValidateTokenResponse {
- isValid: boolean;
- userInfo?: UserInfo;
-}
diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts
deleted file mode 100644
index f4a85262..00000000
--- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { InternalUserService } from './internal-user.service';
-
-export function createMockInternalUserService(): jasmine.SpyObj<InternalUserService> {
- return jasmine.createSpyObj('InternalUserService', ['userRouteNavigate', 'refreshAndGetUserState', 'tryLogin']);
-}
diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts
deleted file mode 100644
index 15755382..00000000
--- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { TestBed, fakeAsync, tick } from '@angular/core/testing';
-import { HttpRequest } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { Router } from '@angular/router';
-import { MatSnackBar } from '@angular/material';
-
-import { Mock } from 'src/app/test-utilities/mock';
-import { createMockStorage } from 'src/app/test-utilities/storage.mock';
-import { WINDOW } from '../window-inject-token';
-
-import { UserInfo, UserCredentials } from '../entities';
-import {
- createTokenUrl, validateTokenUrl, CreateTokenRequest,
- CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse
-} from './http-entities';
-import { InternalUserService, SnackBarTextKey, snackBarText, TOKEN_STORAGE_KEY } from './internal-user.service';
-import { repeat } from 'src/app/utilities/language-untilities';
-
-
-describe('InternalUserService', () => {
- let mockLocalStorage: Mock<Storage>;
- let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
-
- beforeEach(() => {
- mockLocalStorage = createMockStorage();
- mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['open']);
- TestBed.configureTestingModule({
- imports: [HttpClientTestingModule],
- providers: [
- { provide: WINDOW, useValue: { localStorage: mockLocalStorage } },
- { provide: Router, useValue: null },
- { provide: MatSnackBar, useValue: mockSnackBar }
- ]
- });
- });
-
- it('should be created', () => {
- const service: InternalUserService = TestBed.get(InternalUserService);
- expect(service).toBeTruthy();
- });
-
- const mockUserInfo: UserInfo = {
- username: 'user',
- roles: ['user', 'other']
- };
-
- const mockToken = 'mock-token';
-
- describe('validate token', () => {
- const validateTokenRequestMatcher = (req: HttpRequest<ValidateTokenRequest>): boolean =>
- req.url === validateTokenUrl && req.body !== null && req.body.token === mockToken;
-
- function createTest(
- expectSnackBarTextKey: SnackBarTextKey,
- setStorageToken: boolean,
- setHttpController?: (controller: HttpTestingController) => void
- ): () => void {
- return fakeAsync(() => {
- if (setStorageToken) {
- mockLocalStorage.setItem(TOKEN_STORAGE_KEY, mockToken);
- }
- TestBed.get(InternalUserService);
- const controller = TestBed.get(HttpTestingController) as HttpTestingController;
- if (setHttpController) {
- setHttpController(controller);
- }
- controller.verify();
- tick();
- expect(mockSnackBar.open).toHaveBeenCalledWith(snackBarText[expectSnackBarTextKey], jasmine.anything(), jasmine.anything());
- });
- }
-
- it('no login should work well', createTest('noLogin', false));
- it('already login should work well', createTest('alreadyLogin', true,
- controller => controller.expectOne(validateTokenRequestMatcher).flush(
- <ValidateTokenResponse>{ isValid: true, userInfo: mockUserInfo })));
- it('invalid login should work well', createTest('invalidLogin', true,
- controller => controller.expectOne(validateTokenRequestMatcher).flush(<ValidateTokenResponse>{ isValid: false })));
- it('check fail should work well', createTest('checkFail', true,
- controller => repeat(4, () => {
- controller.expectOne(validateTokenRequestMatcher).error(new ErrorEvent('Network error', { message: 'simulated network error' }));
- })));
- });
-
- describe('login should work well', () => {
- const mockUserCredentials: UserCredentials = {
- username: 'user',
- password: 'user'
- };
-
- function createTest(rememberMe: boolean) {
- return () => {
- const service: InternalUserService = TestBed.get(InternalUserService);
-
- service.tryLogin({ ...mockUserCredentials, rememberMe: rememberMe }).subscribe(result => {
- expect(result).toEqual(mockUserInfo);
- });
-
- const httpController = TestBed.get(HttpTestingController) as HttpTestingController;
-
- httpController.expectOne((request: HttpRequest<CreateTokenRequest>) =>
- request.url === createTokenUrl && request.body !== null &&
- request.body.username === mockUserCredentials.username &&
- request.body.password === mockUserCredentials.password).flush(<CreateTokenResponse>{
- success: true,
- token: mockToken,
- userInfo: mockUserInfo
- });
-
- expect(service.currentUserInfo).toEqual(mockUserInfo);
-
- httpController.verify();
-
- expect(mockLocalStorage.getItem(TOKEN_STORAGE_KEY)).toBe(rememberMe ? mockToken : null);
- };
- }
-
- it('remember me should work well', createTest(true));
- it('not remember me should work well', createTest(false));
- });
-
- // TODO: test on error situations.
-});
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
deleted file mode 100644
index 66eafde9..00000000
--- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { Injectable, Inject } from '@angular/core';
-import { HttpClient, HttpErrorResponse } from '@angular/common/http';
-import { Router } from '@angular/router';
-
-import { Observable, throwError, BehaviorSubject, of } from 'rxjs';
-import { map, catchError, retry, switchMap, tap, filter } from 'rxjs/operators';
-
-import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError, ServerInternalError } from './errors';
-import {
- createTokenUrl, validateTokenUrl, CreateTokenRequest,
- CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse
-} from './http-entities';
-import { UserCredentials, UserInfo } from '../entities';
-import { MatSnackBar } from '@angular/material';
-import { WINDOW } from '../window-inject-token';
-
-export const snackBarText = {
- checkFail: 'Failed to check last login',
- noLogin: 'No login before!',
- alreadyLogin: 'You have login already!',
- invalidLogin: 'Last login is no longer invalid!',
- ok: 'ok'
-};
-
-export type SnackBarTextKey = Exclude<keyof typeof snackBarText, 'ok'>;
-
-export const TOKEN_STORAGE_KEY = 'token';
-
-export interface LoginInfo extends UserCredentials {
- rememberMe: boolean;
-}
-
-/**
- * This service is only used internal in user module.
- */
-@Injectable({
- providedIn: 'root'
-})
-export class InternalUserService {
-
- private token: string | null = null;
- private userInfoSubject = new BehaviorSubject<UserInfo | null | undefined>(undefined);
-
- readonly userInfo$: Observable<UserInfo | null> =
- <Observable<UserInfo | null>>this.userInfoSubject.pipe(filter(value => value !== undefined));
-
- get currentUserInfo(): UserInfo | null | undefined {
- return this.userInfoSubject.value;
- }
-
- private openSnackBar(snackBar: MatSnackBar, textKey: SnackBarTextKey) {
- setTimeout(() => snackBar.open(snackBarText[textKey], snackBarText.ok, { duration: 2000 }), 0);
- }
-
- constructor(@Inject(WINDOW) private window: Window, private httpClient: HttpClient, private router: Router, snackBar: MatSnackBar) {
- const savedToken = this.window.localStorage.getItem(TOKEN_STORAGE_KEY);
- if (savedToken === null) {
- this.openSnackBar(snackBar, 'noLogin');
- this.userInfoSubject.next(null);
- } else {
- this.validateToken(savedToken).subscribe(result => {
- if (result === null) {
- this.window.localStorage.removeItem(TOKEN_STORAGE_KEY);
- this.openSnackBar(snackBar, 'invalidLogin');
- this.userInfoSubject.next(null);
- } else {
- this.token = savedToken;
- this.userInfoSubject.next(result);
- this.openSnackBar(snackBar, 'alreadyLogin');
- }
- }, _ => {
- this.openSnackBar(snackBar, 'checkFail');
- this.userInfoSubject.next(null);
- });
- }
- }
-
- private validateToken(token: string): Observable<UserInfo | null> {
- return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ token: token }).pipe(
- retry(3),
- switchMap(result => {
- if (result.isValid) {
- const { userInfo } = result;
- if (userInfo) {
- return of(userInfo);
- } else {
- return throwError(new ServerInternalError('IsValid is true but UserInfo is null.'));
- }
- } else {
- return of(null);
- }
- }),
- tap({
- error: error => {
- console.error('Failed to validate token.');
- console.error(error);
- }
- }),
- );
- }
-
- userRouteNavigate(commands: any[] | null) {
- this.router.navigate([{
- outlets: {
- user: commands
- }
- }]);
- }
-
- tryLogin(info: LoginInfo): Observable<UserInfo> {
- if (this.token) {
- return throwError(new AlreadyLoginError());
- }
-
- return this.httpClient.post<CreateTokenResponse>(createTokenUrl, <CreateTokenRequest>info).pipe(
- catchError((error: HttpErrorResponse) => {
- if (error.error instanceof ErrorEvent) {
- console.error('An error occurred when login: ' + error.error.message);
- return throwError(new BadNetworkError());
- } else {
- console.error('An unknown error occurred when login: ' + error);
- return throwError(new UnknownError(error));
- }
- }),
- switchMap(result => {
- if (result.success) {
- if (result.token && result.userInfo) {
- this.token = result.token;
- if (info.rememberMe) {
- this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token);
- }
- this.userInfoSubject.next(result.userInfo);
- return of(result.userInfo);
- } else {
- console.error('An error occurred when login: server return wrong data.');
- return throwError(new ServerInternalError('Token or userInfo is null.'));
- }
- } else {
- console.error('An error occurred when login: wrong credentials.');
- return throwError(new BadCredentialsError());
- }
- })
- );
- }
-
- logout() {
- if (this.currentUserInfo === null) {
- throw new Error('No login now. You can\'t logout.');
- }
-
- this.window.localStorage.removeItem(TOKEN_STORAGE_KEY);
- this.token = null;
- this.userInfoSubject.next(null);
- }
-}
diff --git a/Timeline/ClientApp/src/app/user/redirect.component.ts b/Timeline/ClientApp/src/app/user/redirect.component.ts
deleted file mode 100644
index 438b38b9..00000000
--- a/Timeline/ClientApp/src/app/user/redirect.component.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-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-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css
deleted file mode 100644
index e69de29b..00000000
--- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css
+++ /dev/null
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
deleted file mode 100644
index e8dbb003..00000000
--- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html
+++ /dev/null
@@ -1 +0,0 @@
-<router-outlet name="user"></router-outlet>
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
deleted file mode 100644
index 47860eee..00000000
--- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Component } from '@angular/core';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { Router, Event } from '@angular/router';
-
-import { Observable } from 'rxjs';
-
-import { UserDialogComponent } from './user-dialog.component';
-
-@Component({
- /* tslint:disable-next-line:component-selector*/
- selector: 'router-outlet',
- template: ''
-})
-class RouterOutletStubComponent { }
-
-
-describe('UserDialogComponent', () => {
- let component: UserDialogComponent;
- let fixture: ComponentFixture<UserDialogComponent>;
-
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [UserDialogComponent, RouterOutletStubComponent],
- providers: [{ // for the workaround
- provide: Router, useValue: {
- events: new Observable<Event>()
- }
- }]
- })
- .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
deleted file mode 100644
index 2887f0a6..00000000
--- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Component, OnInit, ViewChild } from '@angular/core';
-import { RouterOutlet, Router, ActivationStart } from '@angular/router';
-
-@Component({
- selector: 'app-user-dialog',
- templateUrl: './user-dialog.component.html',
- styleUrls: ['./user-dialog.component.css']
-})
-export class UserDialogComponent implements OnInit {
-
- constructor(private router: Router) { }
-
- @ViewChild(RouterOutlet) outlet!: RouterOutlet;
-
- ngOnInit() {
- // this is a workaround for a bug. see https://github.com/angular/angular/issues/20694
- const subscription = this.router.events.subscribe(e => {
- if (e instanceof ActivationStart && e.snapshot.outlet === 'user') {
- this.outlet.deactivate();
- subscription.unsubscribe();
- }
- });
- }
-}
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
deleted file mode 100644
index b1101e2a..00000000
--- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css
+++ /dev/null
@@ -1,22 +0,0 @@
-.login-success-message {
- color: green;
-}
-
-.username {
- color: blue;
-}
-
-:host {
- display: flex;
- flex-wrap: wrap;
-}
-
-:host p {
- margin-top: 0.3em;
- margin-bottom: 0.3em;
- width: 100%;
-}
-
-.logout-button {
- margin-left: auto;
-}
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
deleted file mode 100644
index 685f6299..00000000
--- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<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 <span class="roles">{{ userInfo.roles.join(', ') }}</span>.</p>
-<a mat-flat-button class="logout-button" [routerLink]="['..','logout']">Logout</a>
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
deleted file mode 100644
index 3eba2696..00000000
--- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { By } from '@angular/platform-browser';
-import { ActivatedRoute } from '@angular/router';
-
-import { RouterLinkStubDirective } from '../../test-utilities/router-link.mock';
-import { MockActivatedRoute } from '../../test-utilities/activated-route.mock';
-import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock';
-
-import { UserLoginSuccessComponent } from './user-login-success.component';
-import { InternalUserService } from '../internal-user-service/internal-user.service';
-
-
-describe('UserLoginSuccessComponent', () => {
- let component: UserLoginSuccessComponent;
- let fixture: ComponentFixture<UserLoginSuccessComponent>;
-
- let mockInternalUserService: jasmine.SpyObj<InternalUserService>;
- let mockActivatedRoute: MockActivatedRoute;
-
- const mockUserInfo = {
- username: 'crupest',
- roles: ['superman', 'coder']
- };
-
- beforeEach(async(() => {
- mockInternalUserService = createMockInternalUserService();
- mockActivatedRoute = new MockActivatedRoute();
-
- // mock currentUserInfo property. because it only has a getter so cast it to any first.
- (<any>mockInternalUserService).currentUserInfo = mockUserInfo;
-
- TestBed.configureTestingModule({
- declarations: [UserLoginSuccessComponent, RouterLinkStubDirective],
- providers: [
- { provide: InternalUserService, useValue: mockInternalUserService },
- { provide: ActivatedRoute, useValue: mockActivatedRoute }
- ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(UserLoginSuccessComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- fixture.detectChanges();
- expect(component).toBeTruthy();
- });
-
- it('user info should work well', () => {
- fixture.detectChanges();
-
- expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeFalsy();
-
- expect((fixture.debugElement.query(By.css('span.username')).nativeElement as HTMLSpanElement).textContent)
- .toBe(mockUserInfo.username);
- expect((fixture.debugElement.query(By.css('span.roles')).nativeElement as HTMLSpanElement).textContent)
- .toBe(mockUserInfo.roles.join(', '));
- });
-
- it('login success message should display well', () => {
- mockActivatedRoute.pushSnapshotWithParamMap({ fromlogin: 'true' });
- fixture.detectChanges();
- expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeTruthy();
- });
-
- it('logout button should be set well', () => {
- fixture.detectChanges();
- const routerLinkDirective: RouterLinkStubDirective =
- fixture.debugElement.query(By.css('a')).injector.get(RouterLinkStubDirective);
- expect(routerLinkDirective.linkParams).toEqual(['..', 'logout']);
- });
-});
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
deleted file mode 100644
index 2ae584d6..00000000
--- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import { UserInfo } from '../entities';
-import { InternalUserService } from '../internal-user-service/internal-user.service';
-import { throwIfNullOrUndefined } from 'src/app/utilities/language-untilities';
-
-@Component({
- selector: 'app-user-login-success',
- templateUrl: './user-login-success.component.html',
- styleUrls: ['./user-login-success.component.css']
-})
-export class UserLoginSuccessComponent implements OnInit {
-
- displayLoginSuccessMessage = false;
-
- userInfo!: UserInfo;
-
- constructor(private route: ActivatedRoute, private userService: InternalUserService) { }
-
- ngOnInit() {
- this.userInfo = throwIfNullOrUndefined(this.userService.currentUserInfo, () => 'Route error. No login now!');
- this.displayLoginSuccessMessage = this.route.snapshot.paramMap.get('fromlogin') === 'true';
- }
-}
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
deleted file mode 100644
index 8bf6b408..00000000
--- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.css
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index 7398ece7..00000000
--- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<form [formGroup]="form">
- <ng-container *ngIf="message" [ngSwitch]="message">
- <p *ngSwitchCase="'nologin'" class="mat-h3 no-login-message">You haven't login.</p>
- <p *ngSwitchCase="'invalidlogin'" class="mat-h3 invalid-login-message">Your login is no longer valid.</p>
- <p *ngSwitchDefault class="mat-h3 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>
- <mat-checkbox formControlName="rememberMe">Remember me!</mat-checkbox>
- <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
deleted file mode 100644
index f010e4b7..00000000
--- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { ReactiveFormsModule } from '@angular/forms';
-import { By } from '@angular/platform-browser';
-
-import { of, throwError } from 'rxjs';
-
-import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock';
-import { UserLoginComponent } from './user-login.component';
-import { InternalUserService } from '../internal-user-service/internal-user.service';
-import { UserInfo } from '../entities';
-import { MatCheckboxModule } from '@angular/material';
-
-describe('UserLoginComponent', () => {
- let component: UserLoginComponent;
- let fixture: ComponentFixture<UserLoginComponent>;
- let mockInternalUserService: jasmine.SpyObj<InternalUserService>;
-
- beforeEach(async(() => {
- mockInternalUserService = createMockInternalUserService();
-
- // mock property
- (<any>mockInternalUserService).currentUserInfo = null;
-
- TestBed.configureTestingModule({
- declarations: [UserLoginComponent],
- providers: [
- { provide: InternalUserService, useValue: mockInternalUserService }
- ],
- imports: [ReactiveFormsModule, MatCheckboxModule],
- schemas: [NO_ERRORS_SCHEMA]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(UserLoginComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- fixture.detectChanges();
- expect(component).toBeTruthy();
- });
-
- it('reactive form should work well', () => {
- fixture.detectChanges();
-
- const usernameInput = fixture.debugElement.query(By.css('input[type=text]')).nativeElement as HTMLInputElement;
- const passwordInput = fixture.debugElement.query(By.css('input[type=password]')).nativeElement as HTMLInputElement;
- const rememberMeCheckbox = fixture.debugElement.query(By.css('input[type=checkbox]')).nativeElement as HTMLInputElement;
-
- usernameInput.value = 'user';
- usernameInput.dispatchEvent(new Event('input'));
- passwordInput.value = 'user';
- passwordInput.dispatchEvent(new Event('input'));
- rememberMeCheckbox.dispatchEvent(new MouseEvent('click'));
-
- fixture.detectChanges();
-
- expect(component.form.value).toEqual({
- username: 'user',
- password: 'user',
- rememberMe: true
- });
- });
-
- it('login should work well', () => {
- fixture.detectChanges();
-
- const mockValue = {
- username: 'user',
- password: 'user',
- rememberMe: true
- };
-
- mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(of(<UserInfo>{ username: 'user', roles: ['user'] }));
-
- component.form.setValue(mockValue);
- component.onLoginButtonClick();
-
- expect(mockInternalUserService.tryLogin).toHaveBeenCalledWith(mockValue);
- expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['success', { fromlogin: 'true' }]);
- });
-
- describe('message display', () => {
- it('nologin reason should display', () => {
- fixture.detectChanges();
- component.message = 'nologin';
- fixture.detectChanges();
- expect((fixture.debugElement.query(By.css('p')).nativeElement as
- HTMLParagraphElement).textContent).toBe('You haven\'t login.');
- });
-
- it('invalid login reason should display', () => {
- fixture.detectChanges();
- component.message = 'invalidlogin';
- fixture.detectChanges();
- expect((fixture.debugElement.query(By.css('p')).nativeElement as
- HTMLParagraphElement).textContent).toBe('Your login is no longer valid.');
- });
-
- it('custom error message should display', () => {
- const customMessage = 'custom message';
-
- fixture.detectChanges();
-
- const mockValue = {
- username: 'user',
- password: 'user',
- rememberMe: false
- };
- mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(throwError(new Error(customMessage)));
- component.form.setValue(mockValue);
- component.onLoginButtonClick();
-
- fixture.detectChanges();
- expect(component.message).toBe(customMessage);
- expect((fixture.debugElement.query(By.css('p')).nativeElement as
- HTMLParagraphElement).textContent).toBe(customMessage);
- });
- });
-});
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
deleted file mode 100644
index 4395c5cf..00000000
--- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { FormGroup, FormControl } from '@angular/forms';
-
-import { InternalUserService } from '../internal-user-service/internal-user.service';
-
-
-export type LoginMessage = 'nologin' | 'invalidlogin' | string | null | undefined;
-
-@Component({
- selector: 'app-user-login',
- templateUrl: './user-login.component.html',
- styleUrls: ['./user-login.component.css']
-})
-export class UserLoginComponent implements OnInit {
-
- constructor(private userService: InternalUserService) { }
-
- message: LoginMessage;
-
- form = new FormGroup({
- username: new FormControl(''),
- password: new FormControl(''),
- rememberMe: new FormControl(false)
- });
-
- ngOnInit() {
- 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', { fromlogin: 'true' }]);
- }, (error: Error) => this.message = error.message);
- }
-}
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css
deleted file mode 100644
index e69de29b..00000000
--- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css
+++ /dev/null
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
deleted file mode 100644
index 309e5c83..00000000
--- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
+++ /dev/null
@@ -1 +0,0 @@
-<p class="mat-body">Logout successfully!</p>
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
deleted file mode 100644
index 855ea4a1..00000000
--- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { UserLogoutComponent } from './user-logout.component';
-import { InternalUserService } from '../internal-user-service/internal-user.service';
-
-describe('UserLogoutComponent', () => {
- let component: UserLogoutComponent;
- let fixture: ComponentFixture<UserLogoutComponent>;
-
- let mockInternalUserService: jasmine.SpyObj<InternalUserService>;
-
- beforeEach(async(() => {
- mockInternalUserService = jasmine.createSpyObj('InternalUserService', ['logout']);
-
- TestBed.configureTestingModule({
- declarations: [UserLogoutComponent],
- providers: [{ provide: InternalUserService, useValue: mockInternalUserService }]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(UserLogoutComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should logout on init', () => {
- fixture.detectChanges();
- expect(mockInternalUserService.logout).toHaveBeenCalled();
- });
-});
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
deleted file mode 100644
index e004196f..00000000
--- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-
-import { InternalUserService } from '../internal-user-service/internal-user.service';
-
-@Component({
- selector: 'app-user-logout',
- templateUrl: './user-logout.component.html',
- styleUrls: ['./user-logout.component.css']
-})
-export class UserLogoutComponent implements OnInit {
- constructor(private userService: InternalUserService) { }
-
- ngOnInit() {
- this.userService.logout();
- }
-}
diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts
deleted file mode 100644
index 59193380..00000000
--- a/Timeline/ClientApp/src/app/user/user.module.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-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, MatSnackBarModule, MatCheckboxModule
-} 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 { RedirectComponent } from './redirect.component';
-import { UtilityModule } from '../utilities/utility.module';
-import { WINDOW } from './window-inject-token';
-import { UserLogoutComponent } from './user-logout/user-logout.component';
-
-@NgModule({
- declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent, RedirectComponent, UserLogoutComponent],
- imports: [
- RouterModule.forChild([
- { path: 'login', canActivate: [RequireNoLoginGuard], component: UserLoginComponent, outlet: 'user' },
- { path: 'success', canActivate: [RequireLoginGuard], component: UserLoginSuccessComponent, outlet: 'user' },
- { path: 'logout', canActivate: [RequireLoginGuard], component: UserLogoutComponent, outlet: 'user' },
- { path: '**', component: RedirectComponent, outlet: 'user' }
- ]),
- CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule,
- MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatCheckboxModule, MatSnackBarModule,
- UtilityModule
- ],
- providers: [{ provide: WINDOW, useValue: window }],
- exports: [RouterModule],
- entryComponents: [UserDialogComponent]
-})
-export class UserModule { }
diff --git a/Timeline/ClientApp/src/app/user/user.service.ts b/Timeline/ClientApp/src/app/user/user.service.ts
deleted file mode 100644
index 6cae2d31..00000000
--- a/Timeline/ClientApp/src/app/user/user.service.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Injectable } from '@angular/core';
-import { MatDialog, MatDialogRef } from '@angular/material';
-import { Router, ActivationStart } from '@angular/router';
-
-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 {
-
- private dialogRef: MatDialogRef<UserDialogComponent> | null = null;
-
- constructor(router: Router, private dialog: MatDialog, private internalService: InternalUserService) {
- router.events.subscribe(event => {
- if (event instanceof ActivationStart && event.snapshot.outlet === 'user') {
- if (!this.dialogRef) {
- setTimeout(() => this.openUserDialog(), 0);
- }
- }
- });
- }
-
- get currentUserInfo(): UserInfo | null | undefined {
- return this.internalService.currentUserInfo;
- }
-
- get userInfo$(): Observable<UserInfo | null> {
- return this.internalService.userInfo$;
- }
-
- 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();
- });
- }
-}
diff --git a/Timeline/ClientApp/src/app/user/window-inject-token.ts b/Timeline/ClientApp/src/app/user/window-inject-token.ts
deleted file mode 100644
index 9f8723f6..00000000
--- a/Timeline/ClientApp/src/app/user/window-inject-token.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { InjectionToken } from '@angular/core';
-
-export const WINDOW = new InjectionToken<Window>('global window');