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