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 @@
-
+
+
+
+
+
+ Password is required
+
+
+
+
+
+
+
+
+
+ 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 @@
-
+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]);
+ }
+}