diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/user')
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'); |