From 0d55b0128a428053470d02fe9fc3b84fba1129db Mon Sep 17 00:00:00 2001 From: tsvetkovv <11594423+Tsvetkovv@users.noreply.github.com> Date: Sat, 2 May 2020 16:13:24 +0400 Subject: [PATCH] refactor(select-union): extract to separate component --- src/_constants/unions.ts | 16 ++++ src/_helpers/login.guard.ts | 30 +++++++ src/_models/index.ts | 1 - src/_services/alert.service.ts | 2 +- src/_services/authentication.service.ts | 72 +++++++++++----- src/_services/browser-storage.service.spec.ts | 12 +++ src/_services/browser-storage.service.ts | 29 +++++++ src/_services/index.ts | 4 - src/_services/token.service.ts | 31 ++++++- src/_services/user.service.ts | 22 +++-- src/app/app-routing.module.ts | 12 ++- src/app/app.module.ts | 24 +++--- src/environments/environment.ts | 2 +- src/login/index.ts | 1 - src/login/login.component.html | 85 ++++++++----------- src/login/login.component.ts | 83 ++++++------------ src/register/index.ts | 1 - src/register/register.component.html | 76 +++++++++-------- src/register/register.component.ts | 40 ++++----- src/select-union/select-union.component.html | 13 +++ src/select-union/select-union.component.scss | 0 .../select-union.component.spec.ts | 25 ++++++ src/select-union/select-union.component.ts | 26 ++++++ 23 files changed, 385 insertions(+), 222 deletions(-) create mode 100644 src/_constants/unions.ts create mode 100644 src/_helpers/login.guard.ts delete mode 100644 src/_models/index.ts create mode 100644 src/_services/browser-storage.service.spec.ts create mode 100644 src/_services/browser-storage.service.ts delete mode 100644 src/_services/index.ts delete mode 100644 src/login/index.ts delete mode 100644 src/register/index.ts create mode 100644 src/select-union/select-union.component.html create mode 100644 src/select-union/select-union.component.scss create mode 100644 src/select-union/select-union.component.spec.ts create mode 100644 src/select-union/select-union.component.ts diff --git a/src/_constants/unions.ts b/src/_constants/unions.ts new file mode 100644 index 0000000..c242b6b --- /dev/null +++ b/src/_constants/unions.ts @@ -0,0 +1,16 @@ +import {InjectionToken} from '@angular/core'; + +export const unions: UnionMap = { + mydata: {key: 'mydata', name: 'My Data', url: 'https://mydata.webtree.org/applyToken'}, + imprint: {key: 'imprint', name: 'Imprint', url: 'https://imprint.webtree.org/applyToken'} +}; + +export interface Union { + key: string; + name: string; + url: string; +} + +export type UnionMap = Record; + +export const UNIONS_TOKEN = new InjectionToken('unions'); diff --git a/src/_helpers/login.guard.ts b/src/_helpers/login.guard.ts new file mode 100644 index 0000000..fdc9426 --- /dev/null +++ b/src/_helpers/login.guard.ts @@ -0,0 +1,30 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {AuthenticationService} from '../_services/authentication.service'; +import {TokenService} from '../_services/token.service'; + +@Injectable({providedIn: 'root'}) +export class LoginGuard implements CanActivate { + constructor( + private tokenService: TokenService, + private authenticationService: AuthenticationService, + ) { + } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean { + if (!this.tokenService.tokenExists()) { + return true; + } + + return this.tokenService.isTokenValid().pipe( + map(isValid => { + if (isValid) { + return !this.authenticationService.redirectToUnionIfNeeded(route); + } + return true; + }) + ); + } +} diff --git a/src/_models/index.ts b/src/_models/index.ts deleted file mode 100644 index f6b9f36..0000000 --- a/src/_models/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './User'; diff --git a/src/_services/alert.service.ts b/src/_services/alert.service.ts index 34fbe57..20335b1 100644 --- a/src/_services/alert.service.ts +++ b/src/_services/alert.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {MatSnackBar} from '@angular/material'; -@Injectable() +@Injectable({ providedIn: 'root'}) export class AlertService { constructor(private snackBar: MatSnackBar) { diff --git a/src/_services/authentication.service.ts b/src/_services/authentication.service.ts index 5c84b0d..bfde780 100644 --- a/src/_services/authentication.service.ts +++ b/src/_services/authentication.service.ts @@ -1,39 +1,67 @@ -import {Injectable} from '@angular/core'; -import 'rxjs/add/operator/map'; +import {HttpClient, HttpErrorResponse} from '@angular/common/http'; +import {Inject, Injectable} from '@angular/core'; +import {Observable, of} from 'rxjs'; +import {catchError, map} from 'rxjs/operators'; import {TokenService} from './token.service'; -import {HttpClient} from '@angular/common/http'; import {environment} from '../environments/environment'; -import {User} from '../_models'; +import {User} from '../_models/User'; +import {AlertService} from './alert.service'; +import {ActivatedRouteSnapshot} from '@angular/router'; +import {Union, UnionMap, UNIONS_TOKEN} from '../_constants/unions'; +import {DOCUMENT} from '@angular/common'; -@Injectable() +@Injectable({providedIn: 'root'}) export class AuthenticationService { constructor(private http: HttpClient, - private tokenService: TokenService) { + private alertService: AlertService, + private tokenService: TokenService, + @Inject(UNIONS_TOKEN) private unions: UnionMap, + @Inject(DOCUMENT) private document: Document, + ) { } - login(user: User) { - return this.http.post(environment.backendUrl + 'token/new', user, {responseType: 'text'}); + login(user: User): Observable { + return this.http.post<{ token: string }>(environment.backendUrl + 'token/new', user) + .pipe( + map(res => { + if ('token' in res) { + return res.token; + } + return null; + }), + catchError((error: HttpErrorResponse) => { + console.log(error); + if (error.status === 401) { + this.alertService.error(error.error); + return of(null); + } + }) + ); } logout() { this.tokenService.removeToken(); } - async isAuthorized(): Promise { - if (!this.tokenService.tokenExists()) { - return Promise.resolve(this.tokenService.tokenExists()); - } + redirectToUnionIfNeeded(route: ActivatedRouteSnapshot): boolean { + const returnUnion = route.queryParamMap.get('returnUnion'); - return this.http.post(environment.backendUrl + 'checkToken', this.tokenService.getToken()) - .toPromise() - .then(() => { + if (typeof returnUnion === 'string') { + if (returnUnion in this.unions) { + this.redirectToUnion(this.unions[returnUnion]); return true; - }).catch((err) => { - if (err.status !== 401) { - console.error(err); - } - this.tokenService.removeToken(); - return false; - }); + } else { + this.alertService.error(`Unknown union: ${returnUnion}`); + } + } + + return false; + } + + redirectToUnion({url}: Union): void { + const token = this.tokenService.getToken(); + const tokenizedUrl = `${url}#token=${token}`; + + this.document.location.href = tokenizedUrl; } } diff --git a/src/_services/browser-storage.service.spec.ts b/src/_services/browser-storage.service.spec.ts new file mode 100644 index 0000000..0757236 --- /dev/null +++ b/src/_services/browser-storage.service.spec.ts @@ -0,0 +1,12 @@ +import {TestBed} from '@angular/core/testing'; + +import {BrowserStorageService} from './browser-storage.service'; + +describe('BrowserStorageServiceService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: BrowserStorageService = TestBed.get(BrowserStorageService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/_services/browser-storage.service.ts b/src/_services/browser-storage.service.ts new file mode 100644 index 0000000..8549b4c --- /dev/null +++ b/src/_services/browser-storage.service.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable, InjectionToken } from '@angular/core'; + +export const BROWSER_STORAGE = new InjectionToken('Browser Storage', { + providedIn: 'root', + factory: () => localStorage +}); + +@Injectable({ + providedIn: 'root' +}) +export class BrowserStorageService { + constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {} + + get(key: string) { + return this.storage.getItem(key); + } + + set(key: string, value: string) { + return this.storage.setItem(key, value); + } + + remove(key: string) { + return this.storage.removeItem(key); + } + + clear() { + this.storage.clear(); + } +} diff --git a/src/_services/index.ts b/src/_services/index.ts deleted file mode 100644 index c0fd27c..0000000 --- a/src/_services/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './authentication.service'; -export * from './alert.service'; -export * from './user.service'; -export * from './token.service'; diff --git a/src/_services/token.service.ts b/src/_services/token.service.ts index b4cdfc9..323de1b 100644 --- a/src/_services/token.service.ts +++ b/src/_services/token.service.ts @@ -1,22 +1,45 @@ import {Injectable} from '@angular/core'; +import {Observable, of} from 'rxjs'; +import {environment} from '../environments/environment'; +import {catchError, mapTo} from 'rxjs/operators'; +import {HttpClient, HttpErrorResponse} from '@angular/common/http'; +import {BrowserStorageService} from './browser-storage.service'; -@Injectable() +@Injectable({providedIn: 'root'}) export class TokenService { private tokenName = 'token'; + constructor(private http: HttpClient, + private storage: BrowserStorageService) { + } + tokenExists(): boolean { return !!this.getToken(); } getToken(): string { - return localStorage.getItem(this.tokenName); + return this.storage.get(this.tokenName); } saveToken(token: string) { - localStorage.setItem(this.tokenName, token); + this.storage.set(this.tokenName, token); } removeToken(): void { - localStorage.removeItem('token'); + this.storage.remove('token'); + } + + isTokenValid(): Observable { + return this.http.post(environment.backendUrl + 'checkToken', this.getToken()) + .pipe( + mapTo(true), + catchError((err: HttpErrorResponse) => { + if (err.status !== 401) { + console.error(err); + } + this.removeToken(); + return of(false); + }) + ); } } diff --git a/src/_services/user.service.ts b/src/_services/user.service.ts index 7a14038..e2c9b52 100644 --- a/src/_services/user.service.ts +++ b/src/_services/user.service.ts @@ -1,17 +1,29 @@ import {Injectable} from '@angular/core'; -import {User} from '../_models'; -import {HttpClient} from '@angular/common/http'; +import {User} from '../_models/User'; +import {HttpClient, HttpErrorResponse} from '@angular/common/http'; import {Observable} from 'rxjs/Observable'; import {environment} from '../environments/environment'; +import {catchError} from 'rxjs/operators'; +import {of, throwError} from 'rxjs'; -@Injectable() +@Injectable({providedIn: 'root'}) export class UserService { constructor(private http: HttpClient) { } - create(user: User): Observable { + create(user: User): Observable { const url = environment.backendUrl + 'user/register'; - return this.http.post(url, user); + + return this.http.post(url, user).pipe( + catchError((error: HttpErrorResponse) => { + console.error(error); + if (error.status === 400) { + return of(null); + } else { + return throwError(error); + } + }) + ); } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 75afbbf..621e3c5 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,11 +1,14 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; -import {RegisterComponent} from '../register'; -import {LoginComponent} from '../login'; +import {RegisterComponent} from '../register/register.component'; +import {LoginComponent} from '../login/login.component'; +import {SelectUnionComponent} from '../select-union/select-union.component'; +import {LoginGuard} from '../_helpers/login.guard'; const routes: Routes = [ {path: 'register', component: RegisterComponent}, - {path: 'login', component: LoginComponent}, + {path: 'login', component: LoginComponent, canActivate: [LoginGuard]}, + {path: 'select-union', component: SelectUnionComponent}, {path: '**', redirectTo: 'login'} ]; @@ -13,4 +16,5 @@ const routes: Routes = [ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) -export class AppRoutingModule { } +export class AppRoutingModule { +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 12d4c0d..45608b9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,22 +1,23 @@ import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; +import {HttpClientModule} from '@angular/common/http'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {RegisterComponent} from '../register'; -import {AlertService, AuthenticationService, TokenService, UserService} from '../_services'; -import {HttpClientModule} from '@angular/common/http'; -import {Subject} from 'rxjs'; -import {LoginComponent} from '../login'; -import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {RegisterComponent} from '../register/register.component'; +import {LoginComponent} from '../login/login.component'; import {MaterialModule} from './material.module'; +import {unions, UNIONS_TOKEN} from '../_constants/unions'; +import {SelectUnionComponent} from '../select-union/select-union.component'; @NgModule({ declarations: [ AppComponent, LoginComponent, RegisterComponent, + SelectUnionComponent, ], imports: [ BrowserModule, @@ -28,11 +29,10 @@ import {MaterialModule} from './material.module'; MaterialModule ], providers: [ - UserService, - AlertService, - AuthenticationService, - TokenService, - Subject + { + provide: UNIONS_TOKEN, + useValue: unions + } ], bootstrap: [AppComponent] }) diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 7e02ec5..d03135a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -4,7 +4,7 @@ export const environment = { production: false, - backendUrl: 'http://localhost:9000/rest/', + backendUrl: 'https://auth-api.webtree.org/rest/', }; /* diff --git a/src/login/index.ts b/src/login/index.ts deleted file mode 100644 index 69c1644..0000000 --- a/src/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './login.component'; diff --git a/src/login/login.component.html b/src/login/login.component.html index abf49a0..5c58924 100644 --- a/src/login/login.component.html +++ b/src/login/login.component.html @@ -1,52 +1,39 @@ -
-
-

Login

-
-
- - - -
Username is required
-
-
- - - -
Password is required
-
-
- - - Register -
-
-
-
+

Login

+
+

- Select Union to login - - - {{union.name}} - - + + + Username is required + - - -

-
+

+

+ + + + Password is required + + + +

+

+ + + Loading + + Register +

+ diff --git a/src/login/login.component.ts b/src/login/login.component.ts index cd026d5..d0a363b 100644 --- a/src/login/login.component.ts +++ b/src/login/login.component.ts @@ -1,81 +1,50 @@ -import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; +import {Component} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; -import {AlertService, AuthenticationService, TokenService} from '../_services'; -import {User} from '../_models'; +import {User} from '../_models/User'; import {sha512} from 'js-sha512'; -import {FormBuilder} from '@angular/forms'; +import {FormGroup, FormControl, Validators} from '@angular/forms'; +import {AlertService} from '../_services/alert.service'; +import {AuthenticationService} from '../_services/authentication.service'; +import {TokenService} from '../_services/token.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) -export class LoginComponent implements OnInit { - - objectValues = Object.values; - model: User = {}; +export class LoginComponent { + form: FormGroup = new FormGroup({ + username: new FormControl( + null, [Validators.required]), + password: new FormControl( + null, [Validators.required]) + }); loading = false; - returnUnion: string; - unions = { - mydata: {key: 'mydata', name: 'My Data', url: 'https://mydata.webtree.org/applyToken'}, - imprint: {key: 'imprint', name: 'Imprint', url: 'https://imprint.webtree.org/applyToken'} - }; - loggedIn = false; - - @ViewChild('redirectForm', {read: ElementRef, static: true}) redirectForm: ElementRef; constructor(private route: ActivatedRoute, private router: Router, - private formBuilder: FormBuilder, private authenticationService: AuthenticationService, private tokenService: TokenService, private alertService: AlertService) { } - async ngOnInit() { - this.returnUnion = this.route.snapshot.queryParams.returnUnion; - this.loggedIn = await this.authenticationService.isAuthorized(); - this.redirectIfNeeded(); - } - - login() { + onSubmit({username, password}) { this.loading = true; - const user: User = {username: this.model.username, password: sha512(this.model.password)}; - this.authenticationService.login(user) - .subscribe( - res => { - this.tokenService.saveToken(JSON.parse(res).token); - this.alertService.success('Logged in successfully'); - this.redirectIfNeeded(); - }, - error => { + const user: User = {username, password: sha512(password)}; + return this.authenticationService.login(user) + .subscribe(token => { this.loading = false; - console.log(error); - if (error.status === 401) { - this.alertService.error(error.error); - } else { - throw error; + if (token === null) { + this.alertService.error('Invalid username or password'); + return; + } + + this.tokenService.saveToken(token); + this.alertService.success('Logged in successfully'); + if (!this.authenticationService.redirectToUnionIfNeeded(this.route.snapshot)) { + this.router.navigate(['/select-union']); } } ); } - - getToken(): string { - return this.tokenService.getToken(); - } - - redirectIfNeeded() { - if (this.tokenService.tokenExists() && !!this.returnUnion) { - if (this.unions[this.returnUnion]) { - window.location.href = `${this.unions[this.returnUnion].url}#token=${this.tokenService.getToken()}`; - } else { - this.alertService.error('Unknown union ' + this.returnUnion); - } - } - } - - logout() { - this.authenticationService.logout(); - this.loggedIn = false; - } } diff --git a/src/register/index.ts b/src/register/index.ts deleted file mode 100644 index 55388b6..0000000 --- a/src/register/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './register.component'; diff --git a/src/register/register.component.html b/src/register/register.component.html index 4aae697..e4b781d 100644 --- a/src/register/register.component.html +++ b/src/register/register.component.html @@ -1,37 +1,39 @@ -
-

Register

-
-
- - - -
Username is required
-
-
- - - -
Password is required
-
-
- - - Login -
-
-
+

Login

+
+

+ + + + Username is required + + ` + +

+

+ + + + Password is required + + + +

+

+ + + Loading + + Login +

+
diff --git a/src/register/register.component.ts b/src/register/register.component.ts index 20acad2..2af5188 100644 --- a/src/register/register.component.ts +++ b/src/register/register.component.ts @@ -1,46 +1,40 @@ -import {Component, OnInit} from '@angular/core'; +import {Component} from '@angular/core'; import {AlertService} from '../_services/alert.service'; import {UserService} from '../_services/user.service'; -import {User} from '../_models'; +import {User} from '../_models/User'; import {sha512} from 'js-sha512'; +import {FormControl, FormGroup, Validators} from '@angular/forms'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.css'] }) -export class RegisterComponent implements OnInit { - model: User = {}; - +export class RegisterComponent { + form: FormGroup = new FormGroup({ + username: new FormControl( + null, [Validators.required]), + password: new FormControl( + null, [Validators.required]) + }); loading = false; - constructor( - // private router: Router, - private userService: UserService, + constructor(private userService: UserService, private alertService: AlertService) { } - register() { + onSubmit({username, password}) { this.loading = true; - const user: User = {username: this.model.username, password: sha512(this.model.password)}; + const user: User = {username, password: sha512(password)}; this.userService.create(user) .subscribe( data => { - this.alertService.success('Registration successful'); - // this.router.navigate(['/login']); - }, - error => { - this.loading = false; - console.log(error); - if (error.status === 400) { - this.alertService.error(error.error); + if (data === null) { + this.alertService.error('Registration unsuccessful'); } else { - throw error; + this.alertService.success('Registration successful'); } + // this.router.navigate(['/login']); }); } - - ngOnInit(): void { - } - } diff --git a/src/select-union/select-union.component.html b/src/select-union/select-union.component.html new file mode 100644 index 0000000..300d815 --- /dev/null +++ b/src/select-union/select-union.component.html @@ -0,0 +1,13 @@ + + Select Union to login + + + {{union.name}} + + + + + diff --git a/src/select-union/select-union.component.scss b/src/select-union/select-union.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/select-union/select-union.component.spec.ts b/src/select-union/select-union.component.spec.ts new file mode 100644 index 0000000..7dbb9de --- /dev/null +++ b/src/select-union/select-union.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectUnionComponent } from './select-union.component'; + +describe('SelectUnionComponent', () => { + let component: SelectUnionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SelectUnionComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectUnionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/select-union/select-union.component.ts b/src/select-union/select-union.component.ts new file mode 100644 index 0000000..3b0ff95 --- /dev/null +++ b/src/select-union/select-union.component.ts @@ -0,0 +1,26 @@ +import {Component, Inject} from '@angular/core'; +import {AuthenticationService} from '../_services/authentication.service'; +import {Union, UnionMap, UNIONS_TOKEN} from '../_constants/unions'; + +@Component({ + selector: 'app-select-union', + templateUrl: './select-union.component.html', + styleUrls: ['./select-union.component.scss'] +}) +export class SelectUnionComponent { + public unions: Union[] = Object.values(this.unionsMap); + + constructor( + private authenticationService: AuthenticationService, + @Inject(UNIONS_TOKEN) private unionsMap: UnionMap, + ) { + } + + logout() { + this.authenticationService.logout(); + } + + redirectToUnion(unionKey: string) { + this.authenticationService.redirectToUnion(this.unionsMap[unionKey]); + } +}