From 69d582ec26876bb3d63da9261d787fc5fb65768f Mon Sep 17 00:00:00 2001 From: Hanna <hannapb@stud.ntnu.no> Date: Thu, 18 Feb 2021 15:28:55 +0100 Subject: [PATCH 1/8] feat: added user.module and other components. Co-authored-by: Shirajuki <jonnynl@stud.ntnu.no> --- client/src/app/app-routing.module.ts | 11 ++- client/src/app/app.module.ts | 6 +- client/src/app/models/user.model.ts | 78 +++++++++++++++++++ .../user-login-form.component.html | 1 + .../user-login-form.component.scss | 0 .../user-login-form.component.spec.ts | 25 ++++++ .../user-login-form.component.ts | 15 ++++ .../user-profile/user-profile.component.html | 1 + .../user-profile/user-profile.component.scss | 0 .../user-profile.component.spec.ts | 25 ++++++ .../user-profile/user-profile.component.ts | 15 ++++ .../user-registration-form.component.html | 1 + .../user-registration-form.component.scss | 0 .../user-registration-form.component.spec.ts | 25 ++++++ .../user-registration-form.component.ts | 15 ++++ client/src/app/users/user.module.ts | 24 ++++++ client/src/app/users/user.service.spec.ts | 10 +++ client/src/app/users/user.service.ts | 47 +++++++++++ .../src/controllers/postController/index.ts | 2 +- 19 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 client/src/app/models/user.model.ts create mode 100644 client/src/app/users/user-login-form/user-login-form.component.html create mode 100644 client/src/app/users/user-login-form/user-login-form.component.scss create mode 100644 client/src/app/users/user-login-form/user-login-form.component.spec.ts create mode 100644 client/src/app/users/user-login-form/user-login-form.component.ts create mode 100644 client/src/app/users/user-profile/user-profile.component.html create mode 100644 client/src/app/users/user-profile/user-profile.component.scss create mode 100644 client/src/app/users/user-profile/user-profile.component.spec.ts create mode 100644 client/src/app/users/user-profile/user-profile.component.ts create mode 100644 client/src/app/users/user-registration-form/user-registration-form.component.html create mode 100644 client/src/app/users/user-registration-form/user-registration-form.component.scss create mode 100644 client/src/app/users/user-registration-form/user-registration-form.component.spec.ts create mode 100644 client/src/app/users/user-registration-form/user-registration-form.component.ts create mode 100644 client/src/app/users/user.module.ts create mode 100644 client/src/app/users/user.service.spec.ts create mode 100644 client/src/app/users/user.service.ts diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 498abb3..fb537c4 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -3,15 +3,22 @@ import { RouterModule, Routes } from '@angular/router'; import { PostDetailsComponent } from './posts/post-details/post-details.component'; import { PostFormComponent } from './posts/post-form/post-form.component'; import { PostListComponent } from './posts/post-list/post-list.component'; +import { UserRegistrationFormComponent } from './users/user-registration-form/user-registration-form.component'; +import { UserLoginFormComponent } from './users/user-login-form/user-login-form.component'; +import { UserProfileComponent } from './users/user-profile/user-profile.component'; const routes: Routes = [ { path: 'annonse/ny', component: PostFormComponent }, { path: 'annonse', component: PostListComponent }, - { path: 'annonse/:id', component: PostDetailsComponent } + { path: 'annonse/:id', component: PostDetailsComponent }, + + { path: 'profile', component: UserProfileComponent }, + { path: 'register', component: UserRegistrationFormComponent }, + { path: 'login', component: UserLoginFormComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) -export class AppRoutingModule { } +export class AppRoutingModule { } \ No newline at end of file diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index fb29b91..28a13e7 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -5,14 +5,18 @@ import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { PostModule } from './posts/post.module'; +import { UserModule } from './users/user.module'; import { SharedModule } from './shared/shared.module'; + @NgModule({ declarations: [ - AppComponent, + AppComponent ], imports: [ BrowserModule, + UserModule, + AppRoutingModule, PostModule, SharedModule, diff --git a/client/src/app/models/user.model.ts b/client/src/app/models/user.model.ts new file mode 100644 index 0000000..7c391c9 --- /dev/null +++ b/client/src/app/models/user.model.ts @@ -0,0 +1,78 @@ +import { Deserializable } from "./deserializable.model"; +import { Serializable } from "./serializable.model"; + +export class User implements Deserializable, Serializable { + private userid: number; + private username: string; + private email: string; + private password: string; + private create_time: Date; + + constructor(input: any = null) { + if (input) { + this.deserialize(input); + } else { + this.userid = 0; + this.username = null; + this.email = null; + this.password = null; + this.create_time = new Date(); + } + } + + deserialize(input: Object): this { + Object.assign(this, input); + return this; + } + + serialize(): Object { + return { + userid: this.userid, + username: this.username, + email: this.email, + password: this.password, + create_time: this.create_time + }; + } + + get getUserId() { + return this.userid; + } + + set setUserId(userid: number) { + this.userid = userid; + } + + get getUsername() { + return this.username; + } + + set setUsername(username: string) { + this.username = username; + } + + get getEmail() { + return this.email; + } + + set setEmail(email: string) { + this.email = email; + } + + get getPassword() { + return this.password; + } + + set setPassword(password: string) { + this.password = password; + } + + get getCreateTime() { + return this.create_time; + } + + set setCreateTime(create_time: Date) { + this.create_time = create_time; + } + +} \ No newline at end of file diff --git a/client/src/app/users/user-login-form/user-login-form.component.html b/client/src/app/users/user-login-form/user-login-form.component.html new file mode 100644 index 0000000..0df1f07 --- /dev/null +++ b/client/src/app/users/user-login-form/user-login-form.component.html @@ -0,0 +1 @@ +<p>user-login-form works!</p> diff --git a/client/src/app/users/user-login-form/user-login-form.component.scss b/client/src/app/users/user-login-form/user-login-form.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/users/user-login-form/user-login-form.component.spec.ts b/client/src/app/users/user-login-form/user-login-form.component.spec.ts new file mode 100644 index 0000000..1e5bb79 --- /dev/null +++ b/client/src/app/users/user-login-form/user-login-form.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginFormComponent } from './user-login-form.component'; + +describe('UserLoginFormComponent', () => { + let component: UserLoginFormComponent; + let fixture: ComponentFixture<UserLoginFormComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserLoginFormComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserLoginFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/users/user-login-form/user-login-form.component.ts b/client/src/app/users/user-login-form/user-login-form.component.ts new file mode 100644 index 0000000..1176557 --- /dev/null +++ b/client/src/app/users/user-login-form/user-login-form.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-user-login-form', + templateUrl: './user-login-form.component.html', + styleUrls: ['./user-login-form.component.scss'] +}) +export class UserLoginFormComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/client/src/app/users/user-profile/user-profile.component.html b/client/src/app/users/user-profile/user-profile.component.html new file mode 100644 index 0000000..fedcb8b --- /dev/null +++ b/client/src/app/users/user-profile/user-profile.component.html @@ -0,0 +1 @@ +<p>user-profile works!</p> diff --git a/client/src/app/users/user-profile/user-profile.component.scss b/client/src/app/users/user-profile/user-profile.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/users/user-profile/user-profile.component.spec.ts b/client/src/app/users/user-profile/user-profile.component.spec.ts new file mode 100644 index 0000000..f0c36ed --- /dev/null +++ b/client/src/app/users/user-profile/user-profile.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserProfileComponent } from './user-profile.component'; + +describe('UserProfileComponent', () => { + let component: UserProfileComponent; + let fixture: ComponentFixture<UserProfileComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserProfileComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/users/user-profile/user-profile.component.ts b/client/src/app/users/user-profile/user-profile.component.ts new file mode 100644 index 0000000..d7ff774 --- /dev/null +++ b/client/src/app/users/user-profile/user-profile.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-user-profile', + templateUrl: './user-profile.component.html', + styleUrls: ['./user-profile.component.scss'] +}) +export class UserProfileComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.html b/client/src/app/users/user-registration-form/user-registration-form.component.html new file mode 100644 index 0000000..e32d6df --- /dev/null +++ b/client/src/app/users/user-registration-form/user-registration-form.component.html @@ -0,0 +1 @@ +<p>user-registration-form works!</p> diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.scss b/client/src/app/users/user-registration-form/user-registration-form.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.spec.ts b/client/src/app/users/user-registration-form/user-registration-form.component.spec.ts new file mode 100644 index 0000000..142dcbf --- /dev/null +++ b/client/src/app/users/user-registration-form/user-registration-form.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserRegistrationFormComponent } from './user-registration-form.component'; + +describe('UserRegistrationFormComponent', () => { + let component: UserRegistrationFormComponent; + let fixture: ComponentFixture<UserRegistrationFormComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserRegistrationFormComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserRegistrationFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.ts b/client/src/app/users/user-registration-form/user-registration-form.component.ts new file mode 100644 index 0000000..7d164b8 --- /dev/null +++ b/client/src/app/users/user-registration-form/user-registration-form.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-user-registration-form', + templateUrl: './user-registration-form.component.html', + styleUrls: ['./user-registration-form.component.scss'] +}) +export class UserRegistrationFormComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/client/src/app/users/user.module.ts b/client/src/app/users/user.module.ts new file mode 100644 index 0000000..3356ea2 --- /dev/null +++ b/client/src/app/users/user.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { FormsModule } from '@angular/forms'; +import { UserRegistrationFormComponent } from './user-registration-form/user-registration-form.component'; +import { UserProfileComponent } from './user-profile/user-profile.component'; +import { UserLoginFormComponent } from './user-login-form/user-login-form.component'; + + + +@NgModule({ + declarations: [ + UserRegistrationFormComponent, + UserProfileComponent, + UserLoginFormComponent + ], + imports: [ + CommonModule, + SharedModule, + FormsModule + ] +}) + +export class UserModule { } diff --git a/client/src/app/users/user.service.spec.ts b/client/src/app/users/user.service.spec.ts new file mode 100644 index 0000000..73647c8 --- /dev/null +++ b/client/src/app/users/user.service.spec.ts @@ -0,0 +1,10 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { User } from '../models/user.model'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + +}); + diff --git a/client/src/app/users/user.service.ts b/client/src/app/users/user.service.ts new file mode 100644 index 0000000..3521b5f --- /dev/null +++ b/client/src/app/users/user.service.ts @@ -0,0 +1,47 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { User } from '../models/user.model'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + userUrl = "api/post/" + + constructor(private http: HttpClient) { } + + /** + * Get all users from database. + */ + getAllUsers(): Promise<Array<User>> { + return new Promise<Array<User>>( + (resolve, reject) => { + this.get_all_users().subscribe((data: any) => { + try { + let outputUsers = []; // array of users + for (let user of data.data) { + outputUsers.push(new User(user)); + + if (user.getId == 0) { + reject("Could not deserialize User"); + return; + } + } + resolve(outputUsers); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private get_all_users() { + return this.http.get(this.userUrl); + } +} diff --git a/server/src/controllers/postController/index.ts b/server/src/controllers/postController/index.ts index ed5a95f..7fcf43d 100644 --- a/server/src/controllers/postController/index.ts +++ b/server/src/controllers/postController/index.ts @@ -25,7 +25,7 @@ router.route("/").post(async (request: Request, response: Response) => { description: description, timestamp: timestamp, owner: owner, - category: category, + categoryid: category, imageUrl: imageUrl, }; if (Object.values(post).filter((p) => p == undefined).length > 0) -- GitLab From 9389213928815975a160c674364d69e4599b1cdc Mon Sep 17 00:00:00 2001 From: Jonny Ngo Luong <jonnynl@stud.ntnu.no> Date: Sat, 20 Feb 2021 23:22:31 +0100 Subject: [PATCH 2/8] feat: add user registration form and request (#8) --- client/src/app/app.component.html | 3 + client/src/app/models/user.model.ts | 13 +- .../user-registration-form.component.html | 14 +- .../user-registration-form.component.ts | 58 +- client/src/app/users/user.service.spec.ts | 122 +- client/src/app/users/user.service.ts | 57 +- package-lock.json | 1860 ++++++++--------- 7 files changed, 1187 insertions(+), 940 deletions(-) diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index dda8c3e..f883e04 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -4,6 +4,9 @@ <a href="/">/</a> <a href="/annonse">/annonse</a> <a href="/annonse/ny">/annonse/ny</a> + <a href="/register">/register</a> + <a href="/login">/login</a> + <a href="/profile">/profile</a> </nav> </div> diff --git a/client/src/app/models/user.model.ts b/client/src/app/models/user.model.ts index 7c391c9..8bfe663 100644 --- a/client/src/app/models/user.model.ts +++ b/client/src/app/models/user.model.ts @@ -2,7 +2,7 @@ import { Deserializable } from "./deserializable.model"; import { Serializable } from "./serializable.model"; export class User implements Deserializable, Serializable { - private userid: number; + private userId: number; private username: string; private email: string; private password: string; @@ -12,7 +12,7 @@ export class User implements Deserializable, Serializable { if (input) { this.deserialize(input); } else { - this.userid = 0; + this.userId = 0; this.username = null; this.email = null; this.password = null; @@ -22,12 +22,13 @@ export class User implements Deserializable, Serializable { deserialize(input: Object): this { Object.assign(this, input); + console.log(this); return this; } serialize(): Object { return { - userid: this.userid, + userId: this.userId, username: this.username, email: this.email, password: this.password, @@ -36,11 +37,11 @@ export class User implements Deserializable, Serializable { } get getUserId() { - return this.userid; + return this.userId; } - set setUserId(userid: number) { - this.userid = userid; + set setUserId(userId: number) { + this.userId = userId; } get getUsername() { diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.html b/client/src/app/users/user-registration-form/user-registration-form.component.html index e32d6df..5542f8e 100644 --- a/client/src/app/users/user-registration-form/user-registration-form.component.html +++ b/client/src/app/users/user-registration-form/user-registration-form.component.html @@ -1 +1,13 @@ -<p>user-registration-form works!</p> +<div class="registrationForm"> + <h3>Register en bruker</h3> + + <app-text-input [(inputModel)]="username" label="Brukernavn" (blur)="checkForm()"></app-text-input> + + <app-text-input [(inputModel)]="email" label="Epost" (blur)="checkForm()"></app-text-input> + + <app-text-input [(inputModel)]="password" label="Passord" (blur)="checkForm()"></app-text-input> + + <p>{{statusMessage}}</p> + + <app-button (click)="registerUser()" text="Register"></app-button> +</div> diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.ts b/client/src/app/users/user-registration-form/user-registration-form.component.ts index 7d164b8..2774920 100644 --- a/client/src/app/users/user-registration-form/user-registration-form.component.ts +++ b/client/src/app/users/user-registration-form/user-registration-form.component.ts @@ -1,4 +1,7 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { User } from 'src/app/models/user.model'; +import { UserService } from '../user.service'; @Component({ selector: 'app-user-registration-form', @@ -6,10 +9,63 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./user-registration-form.component.scss'] }) export class UserRegistrationFormComponent implements OnInit { + username: string = ""; + email: string = ""; + password: string = ""; - constructor() { } + statusMessage: string = ""; + + constructor(private userService: UserService, private router: Router) { } ngOnInit(): void { } + /** + * Validates form. + */ + checkForm(): boolean { + if (this.username == "") { + this.setStatusMessage("Brukernavn kan ikke være tom"); + return false; + } + else if (this.email == "") { + this.setStatusMessage("Eposten kan ikke være tom"); + return false; + } + else if (this.password == "") { + this.setStatusMessage("Passordet kan ikke være tom"); + return false; + } + + this.setStatusMessage(""); + return true; + } + + /** + * Publishes user if it is valid. + */ + registerUser() { + if (this.checkForm()) { + const newUser = new User({ + username: this.username, + email: this.email, + password: this.password, + }); + + // Adds user to database and changes page afterwards + this.userService.addUser(newUser).then(status => { + console.log("User was added: " + status); + this.router.navigateByUrl("/"); + }).catch(error => { + console.log("Error adding user: " + error); + }); + } + } + + /** + * Sets a status message. + */ + setStatusMessage(message: string) { + this.statusMessage = message; + } } diff --git a/client/src/app/users/user.service.spec.ts b/client/src/app/users/user.service.spec.ts index 73647c8..4896330 100644 --- a/client/src/app/users/user.service.spec.ts +++ b/client/src/app/users/user.service.spec.ts @@ -5,6 +5,126 @@ import { User } from '../models/user.model'; import { UserService } from './user.service'; describe('UserService', () => { - + let service: UserService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ] + }); + service = TestBed.inject(UserService); + httpMock = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getUser', () => { + it('should get an user', () => { + // Gets post and checks values + service.getUser(1).then(user => { + expect(user.getUserId).toBe(1); + expect(user.getUsername).toBe("zorg"); + }).catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/user/1"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [{ + userId: 1, + username: "zorg", + email: "blob@planet.us", + password: "Hyttepine", + create_time: 1613552549000, + }] + }); + }); + + it('should reject on invalid user', () => { + // Gets invalid post, should go to catch + service.getUser(2).then(user => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/user/2"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [{ + userId: 0, + username: "zorg", + email: "blob@planet.us", + password: "Hyttepine", + }] + }); + }); + + it('should reject on http error', () => { + // Gets HTTP error instead of post, should catch + service.getUser(2).then(user => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/user/2"); + expect(req.request.method).toBe("GET"); + req.error(new ErrorEvent("400")); + }); + }); + + describe('addUser', () => { + it('should add an user', () => { + const user = new User({ + userId: 1, + username: "zorg", + email: "blob@planet.us", + password: "Hyttepine", + create_time: 1613552549000, + }); + + // Adds user + service.addUser(user) + .then(post => {}) + .catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/user/"); + expect(req.request.method).toBe("POST"); + expect(req.request.body).toEqual(user.serialize()); + req.flush({ + data: [{ + status: "success" + }] + }); + }); + + it('should reject on http error', () => { + const user = new User({ + userId: 1, + username: "zorg", + email: "blob@planet.us", + password: "Hyttepine", + create_time: 1613552549000, + }); + + // Adds user, gets HTTP error, should catch + service.addUser(user).then(user => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/user/"); + expect(req.request.method).toBe("POST"); + expect(req.request.body).toEqual(user.serialize()); + req.error(new ErrorEvent("400")); + }); + }); + }); diff --git a/client/src/app/users/user.service.ts b/client/src/app/users/user.service.ts index 3521b5f..2767961 100644 --- a/client/src/app/users/user.service.ts +++ b/client/src/app/users/user.service.ts @@ -7,10 +7,65 @@ import { User } from '../models/user.model'; }) export class UserService { - userUrl = "api/post/" + userUrl = "api/user/" constructor(private http: HttpClient) { } + /** + * Adds user to database. + */ + addUser(user: User): Promise<string> { + return new Promise<string>( + (resolve, reject) => { + this.add_user(user).subscribe((data: any) => { + try { + resolve(data.status); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private add_user(user: User) { + return this.http.post(this.userUrl, user.serialize()); + } + + /** + * Get post from database by id. + */ + getUser(id: number): Promise<User> { + return new Promise<User>( + (resolve, reject) => { + this.get_user(id).subscribe((data: any) => { + try { + const user = new User(data.data[0]); + if (user.getUserId == 0) { + reject("Could not find User with id: " + id); + return; + } + resolve(user); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private get_user(id: number) { + return this.http.get(this.userUrl + id); + } + /** * Get all users from database. */ diff --git a/package-lock.json b/package-lock.json index e7387e4..3e53d50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,932 +1,932 @@ { - "name": "sellpoint", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "sellpoint", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "concurrently": "^5.3.0" - } - }, - "node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/concurrently": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", - "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", - "dependencies": { - "chalk": "^2.4.2", - "date-fns": "^2.0.1", - "lodash": "^4.17.15", - "read-pkg": "^4.0.1", - "rxjs": "^6.5.2", - "spawn-command": "^0.0.2-1", - "supports-color": "^6.1.0", - "tree-kill": "^1.2.2", - "yargs": "^13.3.0" - }, - "bin": { - "concurrently": "bin/concurrently.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/date-fns": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", - "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==", - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "node_modules/is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", - "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", - "dependencies": { - "normalize-package-data": "^2.3.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "node_modules/resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "dependencies": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "node_modules/spawn-command": { - "version": "0.0.2-1", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" - }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" - }, - "node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" - }, - "node_modules/yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "concurrently": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", - "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", - "requires": { - "chalk": "^2.4.2", - "date-fns": "^2.0.1", - "lodash": "^4.17.15", - "read-pkg": "^4.0.1", - "rxjs": "^6.5.2", - "spawn-command": "^0.0.2-1", - "supports-color": "^6.1.0", - "tree-kill": "^1.2.2", - "yargs": "^13.3.0" - } - }, - "date-fns": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", - "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==" - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "requires": { - "has": "^1.0.3" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "read-pkg": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", - "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", - "requires": { - "normalize-package-data": "^2.3.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - } - }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "spawn-command": { - "version": "0.0.2-1", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } + "name": "sellpoint", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "sellpoint", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "concurrently": "^5.3.0" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/concurrently": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", + "dependencies": { + "chalk": "^2.4.2", + "date-fns": "^2.0.1", + "lodash": "^4.17.15", + "read-pkg": "^4.0.1", + "rxjs": "^6.5.2", + "spawn-command": "^0.0.2-1", + "supports-color": "^6.1.0", + "tree-kill": "^1.2.2", + "yargs": "^13.3.0" + }, + "bin": { + "concurrently": "bin/concurrently.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/date-fns": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", + "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dependencies": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" + }, + "node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "concurrently": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", + "requires": { + "chalk": "^2.4.2", + "date-fns": "^2.0.1", + "lodash": "^4.17.15", + "read-pkg": "^4.0.1", + "rxjs": "^6.5.2", + "spawn-command": "^0.0.2-1", + "supports-color": "^6.1.0", + "tree-kill": "^1.2.2", + "yargs": "^13.3.0" + } + }, + "date-fns": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", + "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==" + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } } -- GitLab From b46209f9fd020893fd2f4dfad31c5f76704605d9 Mon Sep 17 00:00:00 2001 From: Jonny Ngo Luong <jonnynl@stud.ntnu.no> Date: Sat, 20 Feb 2021 23:50:29 +0100 Subject: [PATCH 3/8] feat: added user login form, request and endpoint (#8) --- .../user-login-form.component.html | 12 ++++- .../user-login-form.component.ts | 52 ++++++++++++++++++- .../user-registration-form.component.ts | 2 +- client/src/app/users/user.service.ts | 33 +++++++++++- .../src/controllers/userController/index.ts | 17 ++++-- 5 files changed, 108 insertions(+), 8 deletions(-) diff --git a/client/src/app/users/user-login-form/user-login-form.component.html b/client/src/app/users/user-login-form/user-login-form.component.html index 0df1f07..eef16a7 100644 --- a/client/src/app/users/user-login-form/user-login-form.component.html +++ b/client/src/app/users/user-login-form/user-login-form.component.html @@ -1 +1,11 @@ -<p>user-login-form works!</p> +<div class="loginForm"> + <h3>Login</h3> + + <app-text-input [(inputModel)]="username" label="Brukernavn" (blur)="checkForm()"></app-text-input> + + <app-text-input [(inputModel)]="password" label="Passord" (blur)="checkForm()"></app-text-input> + + <p>{{statusMessage}}</p> + + <app-button (click)="loginUser()" text="Login"></app-button> +</div> diff --git a/client/src/app/users/user-login-form/user-login-form.component.ts b/client/src/app/users/user-login-form/user-login-form.component.ts index 1176557..24057cb 100644 --- a/client/src/app/users/user-login-form/user-login-form.component.ts +++ b/client/src/app/users/user-login-form/user-login-form.component.ts @@ -1,4 +1,7 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { User } from 'src/app/models/user.model'; +import { UserService } from '../user.service'; @Component({ selector: 'app-user-login-form', @@ -6,10 +9,57 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./user-login-form.component.scss'] }) export class UserLoginFormComponent implements OnInit { + username: string = ""; + password: string = ""; - constructor() { } + statusMessage: string = ""; + + constructor(private userService: UserService, private router: Router) { } ngOnInit(): void { } + /** + * Validates form. + */ + checkForm(): boolean { + if (this.username == "") { + this.setStatusMessage("Brukernavn kan ikke være tom"); + return false; + } + else if (this.password == "") { + this.setStatusMessage("Passordet kan ikke være tom"); + return false; + } + + this.setStatusMessage(""); + return true; + } + + /** + * Login the user if it is valid. + */ + loginUser() { + if (this.checkForm()) { + const request = { + username: this.username, + password: this.password, + }; + + // Adds user to database and changes page afterwards + this.userService.login(request).then(status => { + console.log("User login: " + JSON.stringify(status)); + this.router.navigateByUrl("/"); + }).catch(error => { + console.log("Error adding user: " + error); + }); + } + } + + /** + * Sets a status message. + */ + setStatusMessage(message: string) { + this.statusMessage = message; + } } diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.ts b/client/src/app/users/user-registration-form/user-registration-form.component.ts index 2774920..ac3c25c 100644 --- a/client/src/app/users/user-registration-form/user-registration-form.component.ts +++ b/client/src/app/users/user-registration-form/user-registration-form.component.ts @@ -54,7 +54,7 @@ export class UserRegistrationFormComponent implements OnInit { // Adds user to database and changes page afterwards this.userService.addUser(newUser).then(status => { - console.log("User was added: " + status); + console.log("User was added: " + JSON.stringify(status)); this.router.navigateByUrl("/"); }).catch(error => { console.log("Error adding user: " + error); diff --git a/client/src/app/users/user.service.ts b/client/src/app/users/user.service.ts index 2767961..d5ad7a4 100644 --- a/client/src/app/users/user.service.ts +++ b/client/src/app/users/user.service.ts @@ -2,15 +2,44 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { User } from '../models/user.model'; +interface IUserLogin { + username: string; + password: string; +} + @Injectable({ providedIn: 'root' }) export class UserService { - userUrl = "api/user/" + loginUrl = "api/user/login" constructor(private http: HttpClient) { } + /** + * Get request of user from database on login request. + */ + login(body: IUserLogin): Promise<string> { + return new Promise<string>( + (resolve, reject) => { + this.login_user(body).subscribe((data: any) => { + try { + resolve(data.data); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private login_user(body: IUserLogin) { + return this.http.post(this.loginUrl, body); + } /** * Adds user to database. */ @@ -19,7 +48,7 @@ export class UserService { (resolve, reject) => { this.add_user(user).subscribe((data: any) => { try { - resolve(data.status); + resolve(data.data); } catch (err: any) { reject(err); } diff --git a/server/src/controllers/userController/index.ts b/server/src/controllers/userController/index.ts index 8bc393a..f45e65b 100644 --- a/server/src/controllers/userController/index.ts +++ b/server/src/controllers/userController/index.ts @@ -27,24 +27,35 @@ router.route('/').post(async (request: Request, response: Response) => { // Get all users `/api/user/` router.route('/').get(async (_: Request, response: Response) => { try { - const input = "SELECT * FROM user;" + const input = "SELECT userId, username, email, create_time FROM user;" response.status(200).json(await query(input,"")); } catch (error) { response.status(400).send("Bad Request"); } }); -// Get post with id `/api/user/:id` +// Get user with id `/api/user/:id` router.route('/:userId').get(async (request: Request, response: Response) => { const userId = request.params.userId; try { - const input = `SELECT * FROM user WHERE userId=?;` + const input = `SELECT userId, username, email, create_time FROM user WHERE userId=?;` response.status(200).json(await query(input,[userId])); } catch (error) { response.status(400).send("Bad Request"); } }); +// Get user with username and password `/api/user/` +router.route('/login').post(async (request: Request, response: Response) => { + const {username, password} = request.body; + try { + const input = "SELECT userId, username, email, create_time FROM user WHERE username=? AND password=?;" + response.status(200).json(await query(input,[username, password])); + } catch (error) { + response.status(400).send("Bad Request"); + } +}); + /* ============================= UPDATE ============================= */ // Update user from id `/api/user/:id` router.route('/:userId').put(async (request: Request, response: Response) => { -- GitLab From 0e16c5f48319a95640730ed5e3b8d90608517a5b Mon Sep 17 00:00:00 2001 From: Jonny Ngo Luong <jonnynl@stud.ntnu.no> Date: Mon, 22 Feb 2021 01:17:40 +0100 Subject: [PATCH 4/8] feat: added jwt token as session and authentication token (#9) --- .gitignore | 5 + client/src/app/app.module.ts | 3 +- client/src/app/authentication/auth.module.ts | 12 + .../app/authentication/auth.service.spec.ts | 16 ++ client/src/app/authentication/auth.service.ts | 50 ++++ .../user-login-form.component.ts | 15 +- .../user-profile/user-profile.component.html | 3 + .../user-profile/user-profile.component.ts | 19 +- server/package-lock.json | 218 ++++++++++++++++++ server/package.json | 3 + server/src/config.ts | 11 +- .../src/controllers/userController/index.ts | 18 +- server/src/index.ts | 6 +- 13 files changed, 368 insertions(+), 11 deletions(-) create mode 100644 client/src/app/authentication/auth.module.ts create mode 100644 client/src/app/authentication/auth.service.spec.ts create mode 100644 client/src/app/authentication/auth.service.ts diff --git a/.gitignore b/.gitignore index 42b7fa6..3c05752 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,8 @@ yarn-error.log* .idea .vscode + +# keys and .env +jwtRS256.key +jwtRS256.key.pub +server/.env \ No newline at end of file diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 28a13e7..497c6e4 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -6,6 +6,7 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { PostModule } from './posts/post.module'; import { UserModule } from './users/user.module'; +import { AuthModule } from './authentication/auth.module'; import { SharedModule } from './shared/shared.module'; @@ -16,7 +17,7 @@ import { SharedModule } from './shared/shared.module'; imports: [ BrowserModule, UserModule, - + AuthModule, AppRoutingModule, PostModule, SharedModule, diff --git a/client/src/app/authentication/auth.module.ts b/client/src/app/authentication/auth.module.ts new file mode 100644 index 0000000..d2e4fae --- /dev/null +++ b/client/src/app/authentication/auth.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class AuthModule { } diff --git a/client/src/app/authentication/auth.service.spec.ts b/client/src/app/authentication/auth.service.spec.ts new file mode 100644 index 0000000..f1251ca --- /dev/null +++ b/client/src/app/authentication/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/authentication/auth.service.ts b/client/src/app/authentication/auth.service.ts new file mode 100644 index 0000000..626aff1 --- /dev/null +++ b/client/src/app/authentication/auth.service.ts @@ -0,0 +1,50 @@ +import { HttpClient, HttpEvent, HttpInterceptor, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { User } from '../models/user.model'; +import { tap, shareReplay } from 'rxjs/operators'; + +interface IUserLogin { + username: string; + password: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + loginUrl = "api/user/login" + + constructor(private http: HttpClient) { } + + login(body: IUserLogin): Promise<string> { + return new Promise<string>( + (resolve, reject) => { + this.login_user(body).subscribe((data: any) => { + try { + resolve(data.token); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + private login_user(body: IUserLogin) { + return this.http.post(this.loginUrl, body).pipe( + tap(res =>this.setSession(res)), + shareReplay());; + } + private setSession(authResult) { + console.log(authResult); + localStorage.setItem('token', authResult.token); + } + + logout() { + localStorage.removeItem("token"); + } + +} diff --git a/client/src/app/users/user-login-form/user-login-form.component.ts b/client/src/app/users/user-login-form/user-login-form.component.ts index 24057cb..328d380 100644 --- a/client/src/app/users/user-login-form/user-login-form.component.ts +++ b/client/src/app/users/user-login-form/user-login-form.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { User } from 'src/app/models/user.model'; import { UserService } from '../user.service'; +import { AuthService } from '../../authentication/auth.service'; @Component({ selector: 'app-user-login-form', @@ -14,7 +15,7 @@ export class UserLoginFormComponent implements OnInit { statusMessage: string = ""; - constructor(private userService: UserService, private router: Router) { } + constructor(private userService: UserService, private authService: AuthService, private router: Router) { } ngOnInit(): void { } @@ -46,13 +47,21 @@ export class UserLoginFormComponent implements OnInit { password: this.password, }; - // Adds user to database and changes page afterwards + // Login the user + this.authService.login(request).then(status => { + console.log("User login1: " + JSON.stringify(status)); + this.router.navigateByUrl("/"); + }).catch(error => { + console.log("Error user login: " + error); + }); + /* Old this.userService.login(request).then(status => { - console.log("User login: " + JSON.stringify(status)); + console.log("User login2: " + JSON.stringify(status)); this.router.navigateByUrl("/"); }).catch(error => { console.log("Error adding user: " + error); }); + */ } } diff --git a/client/src/app/users/user-profile/user-profile.component.html b/client/src/app/users/user-profile/user-profile.component.html index fedcb8b..9b8f350 100644 --- a/client/src/app/users/user-profile/user-profile.component.html +++ b/client/src/app/users/user-profile/user-profile.component.html @@ -1 +1,4 @@ <p>user-profile works!</p> +<p>Userid: {{user.getUserId}}</p> +<p>username: {{user.getUsername}}</p> +<p>email: {{user.getEmail}}</p> \ No newline at end of file diff --git a/client/src/app/users/user-profile/user-profile.component.ts b/client/src/app/users/user-profile/user-profile.component.ts index d7ff774..d818b70 100644 --- a/client/src/app/users/user-profile/user-profile.component.ts +++ b/client/src/app/users/user-profile/user-profile.component.ts @@ -1,4 +1,9 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from 'src/app/authentication/auth.service'; +import { User } from 'src/app/models/user.model'; +import { UserService } from '../user.service'; + @Component({ selector: 'app-user-profile', @@ -7,9 +12,21 @@ import { Component, OnInit } from '@angular/core'; }) export class UserProfileComponent implements OnInit { - constructor() { } + user: User = new User(); + constructor(private userService: UserService, private authService: AuthService, private router: Router) { } ngOnInit(): void { + const token = localStorage.getItem('token'); + if (!token) this.router.navigateByUrl("/"); + // Get user data from JWT token + const user_data = JSON.parse(atob(token.split(".")[1])).data[0]; + console.log(user_data) + // Gets all categories and displays them in dropdown + this.userService.getUser(user_data.userId).then(user => { + this.user = user; + }).catch (error => { + console.log("Error getting user: " + error); + }); } } diff --git a/server/package-lock.json b/server/package-lock.json index 4908b6d..95c60c7 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,14 +15,17 @@ "@types/supertest": "^2.0.10", "body-parser": "^1.19.0", "cors": "^2.8.5", + "dotenv": "^8.2.0", "express": "^4.17.1", "jest": "^26.6.3", + "jsonwebtoken": "^8.5.1", "mysql": "^2.18.1", "mysql2": "^2.2.5", "supertest": "^6.1.3", "ts-jest": "^26.5.1" }, "devDependencies": { + "@types/jsonwebtoken": "^8.5.0", "nodemon": "^2.0.7", "ts-node": "^9.1.1", "typescript": "^4.1.3" @@ -1062,6 +1065,15 @@ "pretty-format": "^26.0.0" } }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", + "integrity": "sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -1697,6 +1709,11 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -2387,6 +2404,14 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "engines": { + "node": ">=8" + } + }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -2402,6 +2427,14 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4904,6 +4937,32 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -4918,6 +4977,25 @@ "verror": "1.10.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -4996,6 +5074,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8997,6 +9110,15 @@ "pretty-format": "^26.0.0" } }, + "@types/jsonwebtoken": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", + "integrity": "sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -9501,6 +9623,11 @@ "node-int64": "^0.4.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -10039,6 +10166,11 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -10054,6 +10186,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -11928,6 +12068,30 @@ "minimist": "^1.2.5" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -11939,6 +12103,25 @@ "verror": "1.10.0" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -11999,6 +12182,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", diff --git a/server/package.json b/server/package.json index ddee435..7b7e5cc 100644 --- a/server/package.json +++ b/server/package.json @@ -17,14 +17,17 @@ "@types/supertest": "^2.0.10", "body-parser": "^1.19.0", "cors": "^2.8.5", + "dotenv": "^8.2.0", "express": "^4.17.1", "jest": "^26.6.3", + "jsonwebtoken": "^8.5.1", "mysql": "^2.18.1", "mysql2": "^2.2.5", "supertest": "^6.1.3", "ts-jest": "^26.5.1" }, "devDependencies": { + "@types/jsonwebtoken": "^8.5.0", "nodemon": "^2.0.7", "ts-node": "^9.1.1", "typescript": "^4.1.3" diff --git a/server/src/config.ts b/server/src/config.ts index 3ca155c..f2dc97f 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,6 +1,8 @@ +const { config } = require('dotenv'); +config({ path: __dirname+'/../.env'}); const env = process.env; -const config = { +export default { db: { host: env.DB_HOST || 'mysql.stud.ntnu.no', user: env.DB_USER || 'jonnynl_tdt4140', @@ -12,6 +14,9 @@ const config = { debug: false }, listPerPage: 10, + JWT_KEY : env.JWT_KEY || "", + HOST: env.HOST || "localhost", + PORT: env.HTTPPORT || 3000, + ACCESS_TOKEN_SECRET: env.ACCESS_TOKEN_SECRET, + REFRESH_TOKEN_SECRET: env.REFRESH_TOKEN_SECRET, }; - -export default config; diff --git a/server/src/controllers/userController/index.ts b/server/src/controllers/userController/index.ts index f45e65b..6f2b01e 100644 --- a/server/src/controllers/userController/index.ts +++ b/server/src/controllers/userController/index.ts @@ -2,6 +2,8 @@ import { Response, Request } from "express"; import query from '../../services/db_query'; import express from 'express'; import IUser from '../../models/user'; +import * as jwt from 'jsonwebtoken'; +import config from '../../config'; const router = express.Router(); /* ============================= CREATE ============================= */ @@ -50,9 +52,23 @@ router.route('/login').post(async (request: Request, response: Response) => { const {username, password} = request.body; try { const input = "SELECT userId, username, email, create_time FROM user WHERE username=? AND password=?;" - response.status(200).json(await query(input,[username, password])); + const user = await query(input,[username, password]); + // Check if an user object is retrieved + const userObj = Object.values(JSON.parse(JSON.stringify(user.data)))[0]; + if (userObj) { + const jwt_token = jwt.sign({data: user.data}, config.JWT_KEY.replace(/\\n/gm, '\n'), { + algorithm: 'RS256', + expiresIn: 3600*24, // 24 hours + }); + response.status(200).json({ + token: jwt_token, + }); + } else { + response.status(403).send("Invalid combination of username and password given!"); + } } catch (error) { response.status(400).send("Bad Request"); + console.log(error); } }); diff --git a/server/src/index.ts b/server/src/index.ts index d4b4977..77bda9f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,8 +1,10 @@ import app from './app'; +import config from './config'; // REST API config -const port = 3000; +const port = config.PORT; app.listen(port, () => { - console.log(`Listening on port ${port}!`) + const host = config.HOST; + console.log('[*] Server listening at http://%s:%s \n', host, port); }); \ No newline at end of file -- GitLab From f35dfcbfe40e09fb764d9c3f7a3905476cc2fa78 Mon Sep 17 00:00:00 2001 From: Jonny Ngo Luong <jonnynl@stud.ntnu.no> Date: Mon, 22 Feb 2021 14:27:48 +0100 Subject: [PATCH 5/8] feat: implemented jwt token expiration checker (#9) --- client/src/app/app.component.html | 2 +- client/src/app/app.component.ts | 5 ++-- client/src/app/authentication/auth.service.ts | 24 ++++++++++++++++-- .../user-profile/user-profile.component.ts | 25 ++++++++++--------- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index f883e04..6ad5852 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -12,4 +12,4 @@ <div class="wrapper"> <router-outlet></router-outlet> -</div> +</div> \ No newline at end of file diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 6f97d4e..cc061cc 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,5 +1,4 @@ -import { Component - } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ selector: 'app-root', @@ -7,5 +6,5 @@ import { Component styleUrls: ['./app.component.scss'] }) export class AppComponent { - title = 'client'; + title: string = 'client'; } diff --git a/client/src/app/authentication/auth.service.ts b/client/src/app/authentication/auth.service.ts index 626aff1..0d86dee 100644 --- a/client/src/app/authentication/auth.service.ts +++ b/client/src/app/authentication/auth.service.ts @@ -2,6 +2,7 @@ import { HttpClient, HttpEvent, HttpInterceptor, HttpResponse } from '@angular/c import { Injectable } from '@angular/core'; import { User } from '../models/user.model'; import { tap, shareReplay } from 'rxjs/operators'; +import { Router } from '@angular/router'; interface IUserLogin { username: string; @@ -14,7 +15,7 @@ interface IUserLogin { export class AuthService { loginUrl = "api/user/login" - constructor(private http: HttpClient) { } + constructor(private http: HttpClient, private router: Router) { } login(body: IUserLogin): Promise<string> { return new Promise<string>( @@ -42,7 +43,26 @@ export class AuthService { console.log(authResult); localStorage.setItem('token', authResult.token); } - + checkTokenExpiration() { + const token = localStorage.getItem("token"); + if (token) { + const {iat, exp} = JSON.parse(atob(token?.split(".")[1])); + if (iat && exp) { + const issued = new Date(iat*1000); + const expires = new Date(exp*1000); + const now = new Date(); + // Expired token + if (now < issued || now >= expires) { + this.logout(); + this.router.navigateByUrl("/"); + return false + } + return true; + } + } + this.router.navigateByUrl("/") + return false + } logout() { localStorage.removeItem("token"); } diff --git a/client/src/app/users/user-profile/user-profile.component.ts b/client/src/app/users/user-profile/user-profile.component.ts index d818b70..53be4cc 100644 --- a/client/src/app/users/user-profile/user-profile.component.ts +++ b/client/src/app/users/user-profile/user-profile.component.ts @@ -16,17 +16,18 @@ export class UserProfileComponent implements OnInit { constructor(private userService: UserService, private authService: AuthService, private router: Router) { } ngOnInit(): void { - const token = localStorage.getItem('token'); - if (!token) this.router.navigateByUrl("/"); - // Get user data from JWT token - const user_data = JSON.parse(atob(token.split(".")[1])).data[0]; - console.log(user_data) - // Gets all categories and displays them in dropdown - this.userService.getUser(user_data.userId).then(user => { - this.user = user; - }).catch (error => { - console.log("Error getting user: " + error); - }); + // Check for token expiration + if (this.authService.checkTokenExpiration()) { // redirects to "/" if token is expired + const token = localStorage.getItem('token'); + // Get user data from JWT token + const user_data = JSON.parse(atob(token.split(".")[1])).data[0]; + // Gets all user information and displays them in the component + this.userService.getUser(user_data.userId).then(user => { + this.user = user; + }).catch (error => { + console.log("Error getting user: " + error); + }); + } + } - } -- GitLab From f25ae129db4a6e06b6b2942294d12feb7c54e33f Mon Sep 17 00:00:00 2001 From: Jonny Ngo Luong <jonnynl@stud.ntnu.no> Date: Mon, 22 Feb 2021 14:28:59 +0100 Subject: [PATCH 6/8] feat: added jwt authorization middleware for requests (#9) --- server/package-lock.json | 91 +++++++++++++++++++ server/package.json | 2 + .../src/controllers/userController/index.ts | 3 +- server/src/middlewares/auth.ts | 10 ++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 server/src/middlewares/auth.ts diff --git a/server/package-lock.json b/server/package-lock.json index 95c60c7..505bf96 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/cors": "^2.8.9", "@types/express": "^4.17.11", + "@types/express-jwt": "^6.0.1", "@types/jest": "^26.0.20", "@types/mysql": "^2.15.17", "@types/supertest": "^2.0.10", @@ -17,6 +18,7 @@ "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "express-jwt": "^6.0.0", "jest": "^26.6.3", "jsonwebtoken": "^8.5.1", "mysql": "^2.18.1", @@ -1017,6 +1019,15 @@ "@types/serve-static": "*" } }, + "node_modules/@types/express-jwt": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-6.0.1.tgz", + "integrity": "sha512-zB/oXzS8/NTWUzAG343frlqUrsygHPeyYMVcbJ8YYk7rF1G15eUapPgWh0HdeFi51ajFkkUOU+Q540z1Eu4hJQ==", + "dependencies": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.18", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz", @@ -1027,6 +1038,14 @@ "@types/range-parser": "*" } }, + "node_modules/@types/express-unless": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", + "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -1415,6 +1434,11 @@ "node": ">=0.10.0" } }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2777,6 +2801,25 @@ "node": ">= 0.10.0" } }, + "node_modules/express-jwt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-6.0.0.tgz", + "integrity": "sha512-C26y9myRjx7CyhZ+BAT3p+gQyRCoDZ7qo8plCvLDaRT6je6ALIAQknT6XLVQGFKwIy/Ux7lvM2MNap5dt0T7gA==", + "dependencies": { + "async": "^1.5.0", + "express-unless": "^0.3.0", + "jsonwebtoken": "^8.1.0", + "lodash.set": "^4.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz", + "integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5109,6 +5152,11 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -9062,6 +9110,15 @@ "@types/serve-static": "*" } }, + "@types/express-jwt": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-6.0.1.tgz", + "integrity": "sha512-zB/oXzS8/NTWUzAG343frlqUrsygHPeyYMVcbJ8YYk7rF1G15eUapPgWh0HdeFi51ajFkkUOU+Q540z1Eu4hJQ==", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, "@types/express-serve-static-core": { "version": "4.17.18", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz", @@ -9072,6 +9129,14 @@ "@types/range-parser": "*" } }, + "@types/express-unless": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", + "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "requires": { + "@types/express": "*" + } + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -9394,6 +9459,11 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10452,6 +10522,22 @@ "vary": "~1.1.2" } }, + "express-jwt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-6.0.0.tgz", + "integrity": "sha512-C26y9myRjx7CyhZ+BAT3p+gQyRCoDZ7qo8plCvLDaRT6je6ALIAQknT6XLVQGFKwIy/Ux7lvM2MNap5dt0T7gA==", + "requires": { + "async": "^1.5.0", + "express-unless": "^0.3.0", + "jsonwebtoken": "^8.1.0", + "lodash.set": "^4.0.0" + } + }, + "express-unless": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz", + "integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=" + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -12217,6 +12303,11 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", diff --git a/server/package.json b/server/package.json index 7b7e5cc..96d322e 100644 --- a/server/package.json +++ b/server/package.json @@ -12,6 +12,7 @@ "dependencies": { "@types/cors": "^2.8.9", "@types/express": "^4.17.11", + "@types/express-jwt": "^6.0.1", "@types/jest": "^26.0.20", "@types/mysql": "^2.15.17", "@types/supertest": "^2.0.10", @@ -19,6 +20,7 @@ "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "express-jwt": "^6.0.0", "jest": "^26.6.3", "jsonwebtoken": "^8.5.1", "mysql": "^2.18.1", diff --git a/server/src/controllers/userController/index.ts b/server/src/controllers/userController/index.ts index 6f2b01e..33b925a 100644 --- a/server/src/controllers/userController/index.ts +++ b/server/src/controllers/userController/index.ts @@ -4,6 +4,7 @@ import express from 'express'; import IUser from '../../models/user'; import * as jwt from 'jsonwebtoken'; import config from '../../config'; +import authenticateToken from '../../middlewares/auth'; const router = express.Router(); /* ============================= CREATE ============================= */ @@ -37,7 +38,7 @@ router.route('/').get(async (_: Request, response: Response) => { }); // Get user with id `/api/user/:id` -router.route('/:userId').get(async (request: Request, response: Response) => { +router.route('/:userId').get(authenticateToken, async (request: Request, response: Response) => { const userId = request.params.userId; try { const input = `SELECT userId, username, email, create_time FROM user WHERE userId=?;` diff --git a/server/src/middlewares/auth.ts b/server/src/middlewares/auth.ts new file mode 100644 index 0000000..023f268 --- /dev/null +++ b/server/src/middlewares/auth.ts @@ -0,0 +1,10 @@ +import expressJwt from 'express-jwt'; +import config from '../config'; + +const JWT_KEY = config.JWT_KEY.replace(/\\n/gm, '\n'); + +const authenticateToken = expressJwt({ + algorithms: ['RS256'], + secret: JWT_KEY, +}); +export default authenticateToken; \ No newline at end of file -- GitLab From befc2df16009f7e3a64f98d2483122eaf6d34afc Mon Sep 17 00:00:00 2001 From: Jonny Ngo Luong <jonnynl@stud.ntnu.no> Date: Mon, 22 Feb 2021 14:30:53 +0100 Subject: [PATCH 7/8] feat: attach an Authorization header on any request sent from frontend to backend (#9) --- client/package-lock.json | 47 +++++++++++++++++++++++++++++------- client/package.json | 1 + client/src/app/app.module.ts | 13 +++++++++- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 5e65f7c..6e9af44 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,6 +15,7 @@ "@angular/platform-browser": "~11.1.1", "@angular/platform-browser-dynamic": "~11.1.1", "@angular/router": "~11.1.1", + "@auth0/angular-jwt": "^5.0.2", "rxjs": "~6.6.0", "tslib": "^2.0.0", "zone.js": "~0.11.3" @@ -434,6 +435,17 @@ "rxjs": "^6.5.3" } }, + "node_modules/@auth0/angular-jwt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-5.0.2.tgz", + "integrity": "sha512-rSamC9mu+gUxoR86AXcIo+KD7xRIro+/iu1F2Ld85YAZEVKlpB5vYG+g0yGaEOqjtQWP/i0H6fi6XMGPVHSYYQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", @@ -19277,6 +19289,14 @@ "tslib": "^2.0.0" } }, + "@auth0/angular-jwt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-5.0.2.tgz", + "integrity": "sha512-rSamC9mu+gUxoR86AXcIo+KD7xRIro+/iu1F2Ld85YAZEVKlpB5vYG+g0yGaEOqjtQWP/i0H6fi6XMGPVHSYYQ==", + "requires": { + "tslib": "^2.0.0" + } + }, "@babel/code-frame": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", @@ -20926,13 +20946,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -21902,7 +21924,8 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", - "dev": true + "dev": true, + "requires": {} }, "class-utils": { "version": "0.3.6", @@ -22090,13 +22113,15 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz", "integrity": "sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==", - "dev": true + "dev": true, + "requires": {} }, "@angular/core": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz", "integrity": "sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==", - "dev": true + "dev": true, + "requires": {} }, "source-map": { "version": "0.5.7", @@ -25216,7 +25241,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "ieee754": { "version": "1.2.1", @@ -26258,7 +26284,8 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.5.4.tgz", "integrity": "sha512-PtilRLno5O6wH3lDihRnz0Ba8oSn0YUJqKjjux1peoYGwo0AQqrWRbdWk/RLzcGlb+onTyXAnHl6M+Hu3UxG/Q==", - "dev": true + "dev": true, + "requires": {} }, "karma-source-map-support": { "version": "1.4.0", @@ -28738,7 +28765,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -34211,7 +34239,8 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz", "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==", - "dev": true + "dev": true, + "requires": {} }, "xml2js": { "version": "0.4.23", diff --git a/client/package.json b/client/package.json index edef3c6..805856f 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "@angular/platform-browser": "~11.1.1", "@angular/platform-browser-dynamic": "~11.1.1", "@angular/router": "~11.1.1", + "@auth0/angular-jwt": "^5.0.2", "rxjs": "~6.6.0", "tslib": "^2.0.0", "zone.js": "~0.11.3" diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 497c6e4..9b89086 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; +import { JwtModule } from "@auth0/angular-jwt"; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -9,6 +10,9 @@ import { UserModule } from './users/user.module'; import { AuthModule } from './authentication/auth.module'; import { SharedModule } from './shared/shared.module'; +export function tokenGetter() { + return localStorage.getItem("token"); +} @NgModule({ declarations: [ @@ -21,7 +25,14 @@ import { SharedModule } from './shared/shared.module'; AppRoutingModule, PostModule, SharedModule, - HttpClientModule + HttpClientModule, + JwtModule.forRoot({ + config: { + tokenGetter: tokenGetter, + allowedDomains: ["localhost"], + disallowedRoutes: [""], + }, + }), ], providers: [], bootstrap: [AppComponent] -- GitLab From c130e2496117d98d4a58cbcf392bbd51d134c50d Mon Sep 17 00:00:00 2001 From: Jonny Ngo Luong <jonnynl@stud.ntnu.no> Date: Mon, 22 Feb 2021 14:59:23 +0100 Subject: [PATCH 8/8] feat: authentication endpoints setup in the backend (#22) --- client/src/app/authentication/auth.service.ts | 28 +++++++- .../user-registration-form.component.ts | 5 +- client/src/app/users/user.service.ts | 52 +-------------- server/src/config.ts | 2 - .../src/controllers/authController/index.ts | 64 +++++++++++++++++++ .../src/controllers/userController/index.ts | 27 -------- server/src/routes/routes.ts | 2 + 7 files changed, 97 insertions(+), 83 deletions(-) create mode 100644 server/src/controllers/authController/index.ts diff --git a/client/src/app/authentication/auth.service.ts b/client/src/app/authentication/auth.service.ts index 0d86dee..ce40111 100644 --- a/client/src/app/authentication/auth.service.ts +++ b/client/src/app/authentication/auth.service.ts @@ -13,7 +13,8 @@ interface IUserLogin { providedIn: 'root' }) export class AuthService { - loginUrl = "api/user/login" + loginUrl = "api/auth/login"; + registrationUrl = "api/auth/register"; constructor(private http: HttpClient, private router: Router) { } @@ -67,4 +68,29 @@ export class AuthService { localStorage.removeItem("token"); } + /** + * Register an user, if not duplicate, add to database. + */ + registerUser(user: User): Promise<string> { + return new Promise<string>( + (resolve, reject) => { + this.register_user(user).subscribe((data: any) => { + try { + resolve(data.data); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private register_user(user: User) { + return this.http.post(this.registrationUrl, user.serialize()); + } + } diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.ts b/client/src/app/users/user-registration-form/user-registration-form.component.ts index ac3c25c..25cf7f6 100644 --- a/client/src/app/users/user-registration-form/user-registration-form.component.ts +++ b/client/src/app/users/user-registration-form/user-registration-form.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { AuthService } from 'src/app/authentication/auth.service'; import { User } from 'src/app/models/user.model'; import { UserService } from '../user.service'; @@ -15,7 +16,7 @@ export class UserRegistrationFormComponent implements OnInit { statusMessage: string = ""; - constructor(private userService: UserService, private router: Router) { } + constructor(private userService: UserService, private authService: AuthService, private router: Router) { } ngOnInit(): void { } @@ -53,7 +54,7 @@ export class UserRegistrationFormComponent implements OnInit { }); // Adds user to database and changes page afterwards - this.userService.addUser(newUser).then(status => { + this.authService.registerUser(newUser).then(status => { console.log("User was added: " + JSON.stringify(status)); this.router.navigateByUrl("/"); }).catch(error => { diff --git a/client/src/app/users/user.service.ts b/client/src/app/users/user.service.ts index d5ad7a4..b56d038 100644 --- a/client/src/app/users/user.service.ts +++ b/client/src/app/users/user.service.ts @@ -12,61 +12,11 @@ interface IUserLogin { }) export class UserService { userUrl = "api/user/" - loginUrl = "api/user/login" constructor(private http: HttpClient) { } /** - * Get request of user from database on login request. - */ - login(body: IUserLogin): Promise<string> { - return new Promise<string>( - (resolve, reject) => { - this.login_user(body).subscribe((data: any) => { - try { - resolve(data.data); - } catch (err: any) { - reject(err); - } - }, - (err: any) => { - console.log(err.message); - reject(err); - }); - } - ); - } - - private login_user(body: IUserLogin) { - return this.http.post(this.loginUrl, body); - } - /** - * Adds user to database. - */ - addUser(user: User): Promise<string> { - return new Promise<string>( - (resolve, reject) => { - this.add_user(user).subscribe((data: any) => { - try { - resolve(data.data); - } catch (err: any) { - reject(err); - } - }, - (err: any) => { - console.log(err.message); - reject(err); - }); - } - ); - } - - private add_user(user: User) { - return this.http.post(this.userUrl, user.serialize()); - } - - /** - * Get post from database by id. + * Get user from database by id. */ getUser(id: number): Promise<User> { return new Promise<User>( diff --git a/server/src/config.ts b/server/src/config.ts index f2dc97f..5efc857 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -17,6 +17,4 @@ export default { JWT_KEY : env.JWT_KEY || "", HOST: env.HOST || "localhost", PORT: env.HTTPPORT || 3000, - ACCESS_TOKEN_SECRET: env.ACCESS_TOKEN_SECRET, - REFRESH_TOKEN_SECRET: env.REFRESH_TOKEN_SECRET, }; diff --git a/server/src/controllers/authController/index.ts b/server/src/controllers/authController/index.ts new file mode 100644 index 0000000..0171c6e --- /dev/null +++ b/server/src/controllers/authController/index.ts @@ -0,0 +1,64 @@ +import { Response, Request } from "express"; +import query from '../../services/db_query'; +import express from 'express'; +import IUser from '../../models/user'; +import * as jwt from 'jsonwebtoken'; +import config from '../../config'; + +const router = express.Router(); + +// Post register user `/api/auth/register` +router.route('/register').post(async (request: Request, response: Response) => { + const {username, email, password, create_time} = request.body; + try { + // Check valid request data parameters + const user_data: IUser = { + "username": username, + "email": email, + "password": password, + }; + if (Object.values(user_data).filter(p => p == undefined).length > 0) return response.status(500).send("Error"); + // Check for user duplicates + const duplicate_input = "SELECT userId, username, email, create_time FROM user WHERE username=? AND password=?;" + const user = await query(duplicate_input,[user_data.username, user_data.password]); + const retrievedUserObj = Object.values(JSON.parse(JSON.stringify(user.data)))[0]; + if (retrievedUserObj) { + return response.status(403).send("There exists an user with the same username or emails given!"); + } + // If there is no duplicates, create new user + const input = (`INSERT INTO user(username, email, password) VALUES (?,?,?)`) + return response.status(200).json( + await query(input,Object.values(user_data)) + ); + } catch (error) { + return response.status(400).send("Bad Request"); + } +}); + +// Post auth token with username and password `/api/auth/login` +router.route('/login').post(async (request: Request, response: Response) => { + const {username, password} = request.body; + try { + const input = "SELECT userId, username, email, create_time FROM user WHERE username=? AND password=?;" + const user = await query(input,[username, password]); + // Check if an user object is retrieved + const userObj = Object.values(JSON.parse(JSON.stringify(user.data)))[0]; + if (userObj) { + const jwt_token = jwt.sign({data: user.data}, config.JWT_KEY.replace(/\\n/gm, '\n'), { + algorithm: 'RS256', + expiresIn: 3600*24, // 24 hours + }); + response.status(200).json({ + token: jwt_token, + }); + } else { + return response.status(401).send("Invalid combination of username and password given!"); + } + } catch (error) { + return response.status(400).send("Bad Request"); + console.log(error); + } +}); + +export default router; + diff --git a/server/src/controllers/userController/index.ts b/server/src/controllers/userController/index.ts index 33b925a..b0d49b9 100644 --- a/server/src/controllers/userController/index.ts +++ b/server/src/controllers/userController/index.ts @@ -2,8 +2,6 @@ import { Response, Request } from "express"; import query from '../../services/db_query'; import express from 'express'; import IUser from '../../models/user'; -import * as jwt from 'jsonwebtoken'; -import config from '../../config'; import authenticateToken from '../../middlewares/auth'; const router = express.Router(); @@ -48,31 +46,6 @@ router.route('/:userId').get(authenticateToken, async (request: Request, respons } }); -// Get user with username and password `/api/user/` -router.route('/login').post(async (request: Request, response: Response) => { - const {username, password} = request.body; - try { - const input = "SELECT userId, username, email, create_time FROM user WHERE username=? AND password=?;" - const user = await query(input,[username, password]); - // Check if an user object is retrieved - const userObj = Object.values(JSON.parse(JSON.stringify(user.data)))[0]; - if (userObj) { - const jwt_token = jwt.sign({data: user.data}, config.JWT_KEY.replace(/\\n/gm, '\n'), { - algorithm: 'RS256', - expiresIn: 3600*24, // 24 hours - }); - response.status(200).json({ - token: jwt_token, - }); - } else { - response.status(403).send("Invalid combination of username and password given!"); - } - } catch (error) { - response.status(400).send("Bad Request"); - console.log(error); - } -}); - /* ============================= UPDATE ============================= */ // Update user from id `/api/user/:id` router.route('/:userId').put(async (request: Request, response: Response) => { diff --git a/server/src/routes/routes.ts b/server/src/routes/routes.ts index fa6b3f5..5a3e995 100644 --- a/server/src/routes/routes.ts +++ b/server/src/routes/routes.ts @@ -1,6 +1,7 @@ import postController from '../controllers/postController'; import categoryController from '../controllers/categoryController'; import userController from '../controllers/userController'; +import authController from '../controllers/authController'; import express from 'express'; const router = express.Router(); @@ -9,5 +10,6 @@ const router = express.Router(); router.use("/post", postController); router.use("/category", categoryController); router.use("/user", userController); +router.use("/auth", authController); export default router; \ No newline at end of file -- GitLab