Categories
JavaScript Nodejs

Use JSON Web Tokens to Make a Secure Web App

With single page front end apps and mobile apps being more popular than ever, the front end is decoupled from the back end. Since almost all web apps need authentication, there needs to be a way for front end or mobile apps to store user identity data in a secure fashion.

JSON Web Tokens (JWT) is one of the most common ways to store authentication data on front end apps. With Node.js there are popular libraries that can generate and verify the JWT by check for its authenticity by check against a secret key stored in the back end and also check for expiry date.

The token is encoded in a standard format that is understood by most apps. It usually contains user identity data like user ID, user name, etc. It is given to the user when the user can successfully complete authentication.

In this piece, we will build an app that uses JWT to store authentication data. On the back end, we will use the Express framework, which runs on Node.js, and the jsonwebtoken package for generating and verify the token. For the front end, we will use the Angular framework and the @auth0/angular-jwt module for Angular. In our app, when the user enters user name and password and they are in our database, then a JWT will be generated from our secret key and returned to the user, and stored on the front end app in local storage. Whenever the user needs to access authenticated routes on the back end, they will need the token. There will be a function in the back end app called middleware to check for a valid token. A valid token is one that is not expired and verifies to be valid against our secret key. There will also be signed up, and user credential settings pages in addition to the login page.


Now that we have our plan, we can begin by creating the front and back end app folders. Make one for each. Then we start writing the back end app. First, we install some packages and generate our Express skeleton code. We run npx express-generator to generate the code. Then we need to install some packages. We do that by running npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors . @babel/register allows us to use the latest JavaScript features. express-jwt generates the JWT and verifies it against a secret.bcrypt does the hashing and salting of our passwords. sequelize is our ORM for doing CRUD. cors allows our Angular app to communicate with our back end by allowing cross-domain communication. dotenv allows us to store environment variables in an .env file. body-parser is needed for Express to parse JSON requests.

Then we make our database migrations. Run npx sequelize-cli init to generate the skeleton code for our database to object mapping. Then we run:

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string

We make another migration and put:

'use strict';

module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.addConstraint(  
        "Users",  
        ["email"],  
        {  
          type: "unique",  
          name: 'emailUnique'  
        }), 
      queryInterface.addConstraint(  
        "Users",  
        ["userName"],  
        {  
          type: "unique",  
          name: 'userNameUnique'  
        }),  
  }, 

  down: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.removeConstraint(  
        "Users",  
        'emailUnique'  
      ), 
      queryInterface.removeConstraint(  
        "Users",  
        'userNameUnique'  
      ),  
    ])  
  }  
};

This makes sure we don’t have duplicate entries with the same username or email.

This creates the User model and will create the Users table once we run npx sequelize-cli db:migrate .

Let’s write some code. Put the following in app.js:

require("babel/register");  
require("babel-polyfill");  
require('dotenv').config();  
const express = require('express');  
const bodyParser = require('body-parser');  
const cors = require('cors');  
const user = require('./controllers/userController');  
const app = express();
app.use(cors())  
app.use(bodyParser.urlencoded({ extended: true }));  
app.use(bodyParser.json());

app.use((req, res, next) => {  
  res.locals.session = req.session;  
  next();  
});  
  
app.use('/user', user);

app.get('*', (req, res) => {  
  res.redirect('/home');  
});

app.listen((process.env.PORT || 8080), () => {  
  console.log('App running on port 8080!');  
});

We need:

require("babel/register");  
require("babel-polyfill");

to use the latest features in JavaScript.

And we need:

require('dotenv').config();

to read our config in an .env file.

This is the entry point. We will create userController in controllers folder shortly.

app.use(‘/user’, user); routes any URL beginning with user to the userController file.

Next, we add the userController.js file:

const express = require('express');  
const bcrypt = require('bcrypt');  
const router = express.Router();  
const models = require('../models');  
const jwt = require('jsonwebtoken');  
import { authCheck } from '../middlewares/authCheck';

router.post('/login', async (req, res) => {  
    const secret = process.env.JWT_SECRET;  
    const userName = req.body.userName;  
    const password = req.body.password;  
    if (!userName || !password) {  
        return res.send({  
            error: 'User name and password required'  
        })  
    }  
    const users = await models.User.findAll({  
        where: {  
            userName  
        }  
    }) 

    const user = users[0];  
    if (!user) {  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    } 

    try {  
        const compareRes = await bcrypt.compare(password, user.hashedPassword);  
        if (compareRes) {  
            const token = jwt.sign(  
                {  
                    data: {  
                        userName,  
                        userId: user.id  
                    }  
                },  
                secret,  
                { expiresIn: 60 * 60 }  
            );  
            return res.send({ token });  
        }  
        else {  
            res.status(401);  
            return res.send({  
                error: 'Invalid username or password'  
            });  
        }  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    }});

router.post('/signup', async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const password = req.body.password;  
    try {  
        const hashedPassword = await bcrypt.hash(password, 10)  
        await models.User.create({  
            userName,  
            email,  
            hashedPassword  
        })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }  
});

router.put('/updateUser', authCheck, async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const token = req.headers.authorization;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        await models.User.update({  
            userName,  
            email  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

router.put('/updatePassword', authCheck, async (req, res) => {  
    const token = req.headers.authorization;  
    const password = req.body.password;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        const hashedPassword = await bcrypt.hash(password, saltRounds)  
        await models.User.update({  
            hashedPassword  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

module.exports = router;

The login route searches for the User entry, then, if it’s found, checks for the hashed password with the compare function of bcrypt. If both are successful, a JWT is generated. The signup route gets JSON payload of username and password and saves it. Note that there is hashing and salting on the password before saving. Passwords should not be stored as plain text. bcrypt.hash takes two arguments. This first is the plain text password, and the second is the number of salt rounds.

updatePassword route is an authenticated route. It checks for the token and, if it’s valid, will continue to save the user’s password by searching for the User with the user ID that matches the decoded token. We will add the authCheck middleware next.

Create a middlewares folder and create authCheck.js inside it.

const jwt = require('jsonwebtoken');  
const secret = process.env.JWT_SECRET;export const authCheck = (req, res, next) => {  
    if (req.headers.authorization) {  
        const token = req.headers.authorization;  
        jwt.verify(token, secret, (err, decoded) => {  
            if (err) {  
                res.send(401);  
            }  
            else {  
                next();  
            }  
        });  
    }  
    else {  
        res.send(401);  
    }  
}

This allows us to check for authentication in authenticated routes without repeating code. We place if in between the URL and our main route code in each authenticated route by importing this and referencing it.

We make an .env file of the root of the back end app folder, with this content:

DB_HOST='localhost'  
DB_NAME='login-app'  
DB_USERNAME='db-username'  
DB_PASSWORD='db-password'  
JWT_SECRET='secret'

The back end app is now complete. Now we move on to the front end Angular app.

Switch to your front end app folder. To build the Angular app, you need the Angular CLI.

To install it, run npm i -g @angular/cli in your Node.js command prompt. Then, run ng new frontend to generate the skeleton code for your front end app.

Also, install @angular/material according to the Angular documentation.

After that, replace the default app.module.ts with the following:

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
import {  
  MatButtonModule,  
  MatCheckboxModule,  
  MatInputModule,  
  MatMenuModule,  
  MatSidenavModule,  
  MatToolbarModule,  
  MatTableModule,  
  MatDialogModule,  
  MAT_DIALOG_DEFAULT_OPTIONS,  
  MatDatepickerModule,  
  MatSelectModule,  
  MatCardModule  
} from '@angular/material';  
import { MatFormFieldModule } from '[@angular/material](http://twitter.com/angular/material)/form-field';  
import { AppRoutingModule } from './app-routing.module';  
import { AppComponent } from './app.component';  
import { StoreModule } from '@ngrx/store';  
import { reducers } from './reducers';  
import { FormsModule } from '@angular/forms';  
import { TopBarComponent } from './top-bar/top-bar.component';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';  
import { SessionService } from './session.service';  
import { HttpReqInterceptor } from './http-req-interceptor';  
import { UserService } from './user.service';  
import { CapitalizePipe } from './capitalize.pipe';

@NgModule({  
  declarations: [  
    AppComponent,  
    TopBarComponent,  
    HomePageComponent,  
    LoginPageComponent,  
    SignUpPageComponent,  
    SettingsPageComponent,  
  ],  
  imports: [  
    BrowserModule,  
    AppRoutingModule,  
    StoreModule.forRoot(reducers),  
    BrowserAnimationsModule,  
    MatButtonModule,  
    MatCheckboxModule,  
    MatFormFieldModule,  
    MatInputModule,  
    MatMenuModule,  
    MatSidenavModule,  
    MatToolbarModule,  
    MatTableModule,  
    FormsModule,  
    HttpClientModule,  
    MatDialogModule,  
    MatDatepickerModule,  
    MatMomentDateModule,  
    MatSelectModule,  
    MatCardModule,  
    NgxMaterialTimepickerModule  
  ],  
  providers: [  
    SessionService,  
    {  
      provide: HTTP_INTERCEPTORS,  
      useClass: HttpReqInterceptor,  
      multi: true  
    },  
    UserService,  
    {  
      provide: MAT_DIALOG_DEFAULT_OPTIONS,  
      useValue: { hasBackdrop: false }  
    },  
  ],  
  bootstrap: [AppComponent],  
})  
export class AppModule { }

This creates all the dependencies and components which we will add. In order to make authenticated requests easy with our token, we create an HTTP request interceptor by creating http-req-interceptor.ts:

import { Injectable } from '@angular/core';  
import {  
    HttpEvent,  
    HttpInterceptor,  
    HttpHandler,  
    HttpResponse,  
    HttpErrorResponse,  
    HttpRequest  
} from '@angular/common/http';  
import { Observable } from 'rxjs';  
import { environment } from '../environments/environment'  
import { map, filter, tap } from 'rxjs/operators';  
import { Router } from '@angular/router';

@Injectable()  
export class HttpReqInterceptor implements HttpInterceptor {  
    constructor(  
        public router: Router  
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
        let modifiedReq = req.clone({}); 
        if (localStorage.getItem('token')) {  
            modifiedReq = modifiedReq.clone({  
                setHeaders: {  
                    authorization: localStorage.getItem('token')  
                }  
            });  
        } 
        return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {  
            if (event instanceof HttpResponse) {}  
        });  
    }  
}

We set the token in all requests except the login request.

In our environments/environment.ts, we have:

export const environment = {  
  production: false,  
  apiUrl: 'http://localhost:8080'
};

This points to our back end’s URL.

Now we need to make our side nav. We want to add @ngrx/store to store the side nav’s state. We install the package by running npm install @ngrx/store --save. We add our reducer by running ng add @ngrx/store to add our reducers.

We add menu-reducers.ts to set the state centrally in our flux store:

const TOGGLE_MENU = 'TOGGLE_MENU';function menuReducer(state, action) {  
    switch (action.type) {  
        case TOGGLE_MENU:  
            state = action.payload;  
            return state;  
        default:  
            return state  
    }  
}

export { menuReducer, TOGGLE_MENU };

To link our reducer to other parts of the app, put the following in index.ts:

import { menuReducer } from './menu-reducer';  
import { tweetsReducer } from './tweets-reducer';export const reducers = {  
  menu: menuReducer,  
};

To get our Material Design look, add the following to style.css:

/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";  
body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}

Between the head tags in index.html, we add:

<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Then we add a service for the user functions, by running ng g service user . The will create user.service.ts . We then put:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { environment } from 'src/environments/environment';  
import { Router } from '[@angular/router](http://twitter.com/angular/router)';  
import { JwtHelperService } from "@auth0/angular-jwt";

const helper = new JwtHelperService();

@Injectable({  
  providedIn: 'root'  
})  
export class UserService { constructor(  
    private http: HttpClient,  
    private router: Router  
  ) { } 

  signUp(data) {  
    return this.http.post(`${environment.apiUrl}/user/signup`, data);  
  } 

  updateUser(data) {  
    return this.http.put(`${environment.apiUrl}/user/updateUser`, data);  
  } 

  updatePassword(data) {  
    return this.http.put(`${environment.apiUrl}/user/updatePassword`, data);  
  } 

  login(data) {  
    return this.http.post(`${environment.apiUrl}/user/login`, data);  
  } 

  logOut() {  
    localStorage.clear();  
    this.router.navigate(['/']);  
  } 

  isAuthenticated() {  
    try {  
      const token = localStorage.getItem('token');  
      const decodedToken = helper.decodeToken(token);  
      const isExpired = helper.isTokenExpired(token);  
      return !!decodedToken && !isExpired;  
    }  
    catch (ex) {  
      return false;  
    }  
  }}

Each function requests a subscription for HTTP request except for the isAuthenticated function which is used to check for the token’s validity.

We also need routing for our app so we can see the pages when we go to the URLs listed below. In app-routing.module.ts, we put:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { TweetsPageComponent } from './tweets-page/tweets-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { PasswordResetRequestPageComponent } from './password-reset-request-page/password-reset-request-page.component';  
import { PasswordResetPageComponent } from './password-reset-page/password-reset-page.component';  
import { IsAuthenticatedGuard } from './is-authenticated.guard';const routes: Routes = [  
  { path: 'login', component: LoginPageComponent },  
  { path: 'signup', component: SignUpPageComponent },  
  { path: 'settings', component: SettingsPageComponent, canActivate: [IsAuthenticatedGuard] },  
  { path: '**', component: HomePageComponent }];

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }

Now we create the parts that are referenced in the file above. We need to prevent people from access authenticated routes without a token, so we need a guard in Angular. We make that by running ng g guard isAuthenticated . This generates is-authenticated.guard.ts.

We put the following in is-authenticated.guard.ts :

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';  
import { Observable } from 'rxjs';  
import { UserService } from './user.service';

@Injectable({  
  providedIn: 'root'  
})  
export class IsAuthenticatedGuard implements CanActivate {  
  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  canActivate(  
    next: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {  
    const isAuthenticated = this.userService.isAuthenticated();  
    if (!isAuthenticated) {  
      localStorage.clear();  
      this.router.navigate(['/']);  
    }  
    return isAuthenticated;  
  }}

This uses our isAuthenticated function from UserService to check for a valid token. If it’s not valid, we clear it and redirect it back to the home page.

Now we create the forms for logging in setting the user data after logging. We run ng g component homePage, ng g component loginPage, ng g component topBar, ng g component signUpPage, and ng g component settingsPage . These are for the forms and the top bar components.

The home page is just a static page. We should have home-page.component.html and home-page.component.tsgenerated after running the commands in our last paragraph.

In home-page.component.html we put:

<div class="center">  
    <h1>Home Page</h1>  
</div>

Now we make our login page. In login-page.component.ts, we put:

<div class="center">  
    <h1>Log In</h1>  
</div>  
<form #loginForm='ngForm' (ngSubmit)='login(loginForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='loginData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='loginData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Log In</button>  
    <a mat-raised-button routerLink='/passwordResetRequest'>Reset Password</a>  
</form>

In login-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';
[@Component](http://twitter.com/Component)({  
  selector: 'app-login-page',  
  templateUrl: './login-page.component.html',  
  styleUrls: ['./login-page.component.scss']  
})  
export class LoginPageComponent implements OnInit {  
  loginData: any = <any>{}; 

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { } 

  ngOnInit() {  
  } 

  login(loginForm: NgForm) {  
    if (loginForm.invalid) {  
      return;  
    }  
    this.userService.login(this.loginData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/settings']);  
      }, err => {  
        alert('Invalid username or password');  
      })  
  }  
}

We make sure that all fields are filled. If they are, the login data will be sent and the token will be saved to local storage if authentication is successful. Otherwise, an error alert will be displayed.

Then in our sign up page, sign-up-page.component.html, we put:

<div class="center">  
    <h1>Sign Up</h1>  
</div>  
<br>  
<form #signUpForm='ngForm' (ngSubmit)='signUp(signUpForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='signUpData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='signUpData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='signUpData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Sign Up</button>  
</form>

and in sign-up-page.component.ts, we put:

import { Component, OnInit } from '[@angular/core](http://twitter.com/angular/core)';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';  
import _ from 'lodash';

@Component({  
  selector: 'app-sign-up-page',  
  templateUrl: './sign-up-page.component.html',  
  styleUrls: ['./sign-up-page.component.scss']  
})  
export class SignUpPageComponent implements OnInit {  
  signUpData: any = <any>{}; constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  } 

  signUp(signUpForm: NgForm) {  
    if (signUpForm.invalid) {  
      return;  
    }  
    this.userService.signUp(this.signUpData)  
      .subscribe(res => {  
        this.login();  
      }, err => {  
        console.log(err);  
        if (  
          _.has(err, 'error.error.errors') &&  
          Array.isArray(err.error.error.errors) &&  
          err.error.error.errors.length > 0  
        ) {  
          alert(err.error.error.errors[0].message);  
        }  
      })  
  } 

  login() {  
    this.userService.login(this.signUpData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/tweets']);  
      })  
  }  
}

These two pieces of code get the sign-up data and send it to the back end, which will save the file if they are all valid.

Similarly, in the settings-page.component.html:

<div class="center">  
    <h1>Settings</h1>  
</div>  
<br>  
<div>  
    <h2>Update User Info</h2>  
</div>  
<br>  
<form #updateUserForm='ngForm' (ngSubmit)='updateUser(updateUserForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='updateUserData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='updateUserData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update User Info</button>  
</form>  
<br><div>  
    <h2>Update Password</h2>  
</div>  
<br>  
<form #updatePasswordForm='ngForm' (ngSubmit)='updatePassword(updatePasswordForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='updatePasswordData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update Password</button>  
</form>  
<br>
<div *ngIf='currentTwitterUser.id' class="title">  
    <h2>Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Different Twitter Account</button>  
    </div>  
</div>  
<div *ngIf='!currentTwitterUser.id' class="title">  
    <h2>Not Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Twitter Account</button>  
    </div>  
</div>

In settings-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { ActivatedRoute, Router } from '@angular/router';  
import { SessionService } from '../session.service';  
import { NgForm } from '@angular/forms';  
import { UserService } from '../user.service';

@Component({  
  selector: 'app-settings-page',  
  templateUrl: './settings-page.component.html',  
  styleUrls: ['./settings-page.component.scss']  
})  
export class SettingsPageComponent implements OnInit {  
  currentTwitterUser: any = <any>{};  
  elements: any[] = [];  
  displayedColumns: string[] = ['key', 'value'];  
  updateUserData: any = <any>{};  
  updatePasswordData: any = <any>{}; constructor(  
    private sessionService: SessionService,  
    private userService: UserService,  
    private router: Router  
  ) {  
  
  } 

  ngOnInit() {  
  
  } 

  updateUser(updateUserForm: NgForm) {  
    if (updateUserForm.invalid) {  
      return;  
    }  
    this.userService.updateUser(this.updateUserData)  
      .subscribe(res => {  
        alert('Updated user info successful.');  
      }, err => {  
        alert('Updated user info failed.');  
      })  
  } 

  updatePassword(updatePasswordForm: NgForm) {  
    if (updatePasswordForm.invalid) {  
      return;  
    }  
    this.userService.updatePassword(this.updatePasswordData)  
      .subscribe(res => {  
        alert('Updated password successful.');  
      }, err => {  
        alert('Updated password failed.');  
      })  
  }  
}

Similar to other pages, this sends a request payload for changing user data and password to our back end.

Finally, to make our top bar, we put the following in top-bar.component.html:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    Twitter Automator  
</mat-toolbar>

And in top-bar.component.ts:

import { Component, OnInit } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from '../reducers/menu-reducer';

@Component({  
  selector: 'app-top-bar',  
  templateUrl: './top-bar.component.html',  
  styleUrls: ['./top-bar.component.scss']  
})  
export class TopBarComponent implements OnInit {  
  menuOpen: boolean; constructor(  
    private store: Store<any>  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  ngOnInit() {  
  } 

  toggleMenu() {  
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });  
  }  
}

In app.component.ts, we put:

import { Component, HostListener } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from './reducers/menu-reducer';  
import { UserService } from './user.service';

@Component({  
  selector: 'app-root',  
  templateUrl: './app.component.html',  
  styleUrls: ['./app.component.scss']  
})  
export class AppComponent {  
  menuOpen: boolean; 

  constructor(  
    private store: Store<any>,  
    private userService: UserService  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  isAuthenticated() {  
    return this.userService.isAuthenticated();  
  }

  @HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

  logOut() {  
    this.userService.logOut();  
  }  
}

and in app.component.html, we have:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    Twitter Automator  
                </b>  
            </li>  
            <li>  
                <a routerLink='/login' *ngIf='!isAuthenticated()'>Log In</a>  
            </li>  
            <li>  
                <a routerLink='/signup' *ngIf='!isAuthenticated()'>Sign Up</a>  
            </li>  
            <li>  
                <a href='#' (click)='logOut()' *ngIf='isAuthenticated()'>Log Out</a>  
            </li>  
            <li>  
                <a routerLink='/tweets' *ngIf='isAuthenticated()'>Tweets</a>  
            </li>  
            <li>  
                <a routerLink='/settings' *ngIf='isAuthenticated()'>Settings</a>  
            </li>  
        </ul>
    </mat-sidenav>  
    <mat-sidenav-content>  
        <app-top-bar></app-top-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This allows us to toggle our side navigation menu. Note that we have:

@HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

to detect clicks out the side nav. If we click outside, i.e. not clicking on any element with those classes, then we close the menu. this.store.dispatch propagates the closed state to all components.

Categories
JavaScript Nodejs

Use GraphQL APIs in Vue.js Apps

GraphQL is a query language made by Facebook for sending requests over the internet. It uses its own query but still sends data over HTTP. It uses one endpoint only for sending data.

The benefits of using GraphQL include being able to specify data types for the data fields you are sending and being able to specify the types of data fields that are returned.

The syntax is easy to understand, and it is simple. The data are still returned in JSON for easy access and manipulation. This is why GraphQL has been gaining traction in recent years.

GraphQL requests are still HTTP requests. However, you are always sending and getting data over one endpoint. Usually, this is the graphql endpoint. All requests are POST requests, no matter if you are getting, manipulating, or deleting data.

To distinguish between getting and manipulating data, GraphQL requests can be classified as queries and mutations. Below is one example of a GraphQL request:

{  
  getPhotos(page: 1) {  
    photos {  
      id  
      fileLocation  
      description  
      tags  
    }  
    page  
    totalPhotos  
  }  
}

In this story, we will build a Vue.js app that uses the GraphQL Jobs API located at https://graphql.jobs /to display jobs data. To start building the app, we first install the Vue CLI by running npm i @vue/cli. We need the latest version of Node.js LTS installed. After that, we run vue create jobs-app to create new Vue.js project files for our app.

Then, we install some libraries we need for our app, which include a GraphQL client, Vue Material, and VeeValidate for form validation. We run:

npm i vue-apollo vue-material vee-validate@2.2.14 graphql-tag

This installs the packages. Vue Apollo is the GraphQL client, and graphQL-tag converts GraphQL query strings into queries that are usable by Vue Apollo.

Next, we are ready to write some code. First, we write some helper code for our components. We add a mixin for making the GraphQL queries to the Jobs API. Create a new folder called mixins, and add a file called jobMixins.js to it. Then in the file, we add:

import { gql } from "apollo-boost";

export const jobsMixin = {  
  methods: {  
    getJobs(type) {  
      const getJobs = gql`  
      query jobs(  
          $input: JobsInput,  
        ){  
          jobs(  
            input: $input  
          ) {  
            id,  
            title,  
            slug,  
            commitment {  
              id,  
              title,  
              slug  
            },  
            cities {  
              name  
            },  
            countries {  
              name  
            },  
            remotes {  
              name  
            },  
            description,  
            applyUrl,  
            company {  
              name  
            }  
          }  
        }  
      `;  
      return this.$apollo.query({  
        query: getJobs,  
        variables: {  
          type  
        }  
      });  
    }, 

    getCompanies() {  
      const getCompanies = gql`  
      query companies{  
          companies {  
            id,  
            name,  
            slug,  
            websiteUrl,  
            logoUrl,  
            twitter,  
            jobs {  
              id,  
              title  
            }  
          }  
        }  
      `;  
      return this.$apollo.query({  
        query: getCompanies  
      });  
    }  
  }  
}

These functions will get the data we require from the GraphQL Jobs API. The gql in front of the string is a tag. A tag is an expression, which is usually a function that is run to map a string into something else.

In this case, it will map the GraphQL query string into a query object that can be used by the Apollo client.

this.$apollo is provided by the Vue Apollo library. It is available since we will include it in main.js.

Next, in the view folder, we create a file called Companies.vue, and we add:

<template>  
  <div class="home">  
    <div class="center">  
      <h1>Companies</h1>  
    </div>  
    <md-card md-with-hover v-for="c in companies" :key="c.id">  
      <md-card-header>  
        <div class="md-title">  
          <img :src="c.logoUrl" class="logo" />  
          {{c.name}}  
        </div>  
        <div class="md-subhead">  
          <a :href="c.websiteUrl">Link</a>  
        </div>  
        <div class="md-subhead">Twitter: {{c.twitter}}</div>  
      </md-card-header><md-card-content>  
        <md-list>  
          <md-list-item>  
            <h2>Jobs</h2>  
          </md-list-item>  
          <md-list-item v-for="j in c.jobs" :key="j.id">{{j.title}}</md-list-item>  
        </md-list>  
      </md-card-content>  
    </md-card>  
  </div>  
</template>

<script>  
import { jobsMixin } from "../mixins/jobsMixin";  
import { photosUrl } from "../helpers/exports";

export default {  
  name: "home",  
  mixins: [jobsMixin],  
  computed: {  
    isFormDirty() {  
      return Object.keys(this.fields).some(key => this.fields[key].dirty);  
    }  
  },  
  async beforeMount() {  
    const response = await this.getCompanies();  
    this.companies = response.data.companies;  
  },  
  data() {  
    return {  
      companies: []  
    };  
  },  
  methods: {}  
};  
</script>

<style lang="scss" scoped>  
.logo {  
  width: 20px;  
}

.md-card-header {  
  padding: 5px 34px;  
}  
</style>

It uses the mixin function that we created to get the companies’ data and displays it to the user.

In Home.vue, we replace the existing code with the following:

<template>  
  <div class="home">  
    <div class="center">  
      <h1>Home</h1>  
    </div>  
    <form @submit="search" novalidate>  
      <md-field :class="{ 'md-invalid': errors.has('term') }">  
        <label for="term">Search</label>  
        <md-input type="text" name="term" v-model="searchData.type" v-validate="'required'"></md-input>  
        <span class="md-error" v-if="errors.has('term')">{{errors.first('term')}}</span>  
      </md-field> <md-button class="md-raised" type="submit">Search</md-button>  
    </form>  
    <br />  
    <md-card md-with-hover v-for="j in jobs" :key="j.id">  
      <md-card-header>  
        <div class="md-title">{{j.title}}</div>  
        <div class="md-subhead">{{j.company.name}}</div>  
        <div class="md-subhead">{{j.commitment.title}}</div>  
        <div class="md-subhead">Cities: {{j.cities.map(c=>c.name).join(', ')}}</div>  
      </md-card-header> <md-card-content>  
        <p>{{j.description}}</p>  
      </md-card-content><md-card-actions>  
        <md-button v-on:click.stop.prevent="goTo(j.applyUrl)">Apply</md-button>  
      </md-card-actions>  
    </md-card>  
  </div>  
</template>

<script>  
import { jobsMixin } from "../mixins/jobsMixin";  
import { photosUrl } from "../helpers/exports";export default {  
  name: "home",  
  mixins: [jobsMixin],  
  computed: {  
    isFormDirty() {  
      return Object.keys(this.fields).some(key => this.fields[key].dirty);  
    }  
  },  
  beforeMount() {},  
  data() {  
    return {  
      searchData: {  
        type: ""  
      },  
      jobs: []  
    };  
  },  
  methods: {  
    async search(evt) {  
      evt.preventDefault();  
      if (!this.isFormDirty || this.errors.items.length > 0) {  
        return;  
      }  
      const { type } = this.searchData;  
      const response = await this.getJobs(this.searchData.type);  
      this.jobs = response.data.jobs;  
    }, goTo(url) {  
      window.open(url, "_blank");  
    }  
  }  
};  
</script>

<style lang="scss">  
.md-card-header {  
  .md-title {  
    color: black !important;  
  }  
}

.md-card {  
  width: 95vw;  
  margin: 0 auto;  
}  
</style>

In the code above, we have a search form to let users search for jobs with the keyword they entered. The results are displayed in the card.

In App.vue, we replace the existing code with the following:

<template>  
  <div id="app">  
    <md-toolbar>  
      <md-button class="md-icon-button" @click="showNavigation = true">  
        <md-icon>menu</md-icon>  
      </md-button>  
      <h3 class="md-title">GraphQL Jobs App</h3>  
    </md-toolbar>  
    <md-drawer :md-active.sync="showNavigation" md-swipeable>  
      <md-toolbar class="md-transparent" md-elevation="0">  
        <span class="md-title">GraphQL Jobs App</span>  
      </md-toolbar><md-list>  
        <md-list-item>  
          <router-link to="/">  
            <span class="md-list-item-text">Home</span>  
          </router-link>  
        </md-list-item><md-list-item>  
          <router-link to="/companies">  
            <span class="md-list-item-text">Companies</span>  
          </router-link>  
        </md-list-item>  
      </md-list>  
    </md-drawer><router-view />  
  </div>  
</template>

<script>  
export default {  
  name: "app",  
  data: () => {  
    return {  
      showNavigation: false  
    };  
  }  
};  
</script>

<style lang="scss">  
.center {  
  text-align: center;  
}

form {  
  width: 95vw;  
  margin: 0 auto;  
}

.md-toolbar.md-theme-default {  
  background: #009688 !important;  
  height: 60px;  
}

.md-title,  
.md-toolbar.md-theme-default .md-icon {  
  color: #fff !important;  
}  
</style>

This adds a top bar and left menu to our app and allows us to toggle the menu. It also allows us to display the pages we created in the router-view element.

In main.js, we put:

import Vue from 'vue'  
import App from './App.vue'  
import router from './router'  
import store from './store'  
import VueMaterial from 'vue-material';  
import VeeValidate from 'vee-validate';  
import 'vue-material/dist/vue-material.min.css'  
import 'vue-material/dist/theme/default.css'  
import VueApollo from 'vue-apollo';  
import ApolloClient from 'apollo-boost';

Vue.config.productionTip = false  
Vue.use(VeeValidate);  
Vue.use(VueMaterial);  
Vue.use(VueApollo);const client = new ApolloClient({  
  uri: '[https://api.graphql.jobs'](https://api.graphql.jobs'),  
  request: operation => {  
    operation.setContext({  
      headers: {  
        authorization: ''  
      },  
    });  
  }  
});\

const apolloProvider = new VueApollo({  
  defaultClient: client,  
})

new Vue({  
  router,  
  store,  
  apolloProvider,  
  render: h => h(App)  
}).$mount('#app')

This adds the libraries we use in the app (such as Vue Material) and adds the Apollo Client to our app so we can use them in our app.

The this.$apollo object is available in our components and mixins because we inserted apolloProvider in the object we use in the argument of new Vue.

In router.js, we put:

import Vue from 'vue'  
import Router from 'vue-router'  
import Home from './views/Home.vue'  
import Companies from './views/Companies.vue'Vue.use(Router)export default new Router({  
  mode: 'history',  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: '/',  
      name: 'home',  
      component: Home  
    },  
    {  
      path: '/companies',  
      name: 'companies',  
      component: Companies  
    }  
  ]  
})

Now we can see the pages we created when we navigate to them.

Categories
JavaScript Nodejs

How to Send Email with SendGrid in Node.js Apps

SendGrid is a great service made by Twilio for sending emails. Rather than setting up your own email server for sending email with your apps, we use SendGrid to do the hard work for us. It also decrease the chance of email ending up in spam since it is a known trustworthy service.

It also has very easy to use libraries for various platforms for sending emails. Node.js is one of the platforms that are supported.

To send emails with SendGrid, install the SendGrid SDK package by running npm i @sendgrid/mail . Then in your code, add const sgMail = require(‘@sendgrid/mail’); to import the installed package.

Then in your code, you send email by:

sgMail.setApiKey(process.env.SENDGRID_API_KEY);  
const msg = {  
  to: email,  
  from: 'email@example.com',  
  subject: 'Example Email',  
  text: `  
    Dear user, Here is your email.  
  `,  
  html: `  
    <p>Dear user,</p> <p>Here is your email.</p>  
  `,  
};  
sgMail.send(msg);

where process.env.SENDGRID_API_KEY is the SendGrid’s API, which should be stored as an environment variable since it is a secret.

Testing is easy since you don’t need to set up a local development email server.

Sending email is this simple and easy with SendGrid API. It is also free if you send small amounts of email, which is a great benefit.

Categories
JavaScript Nodejs

How to Deploy Node.js Web App to Heroku from Bitbucket

Note that the Git command is pushing a subfolder to Heroku, but you can push the root folder as well.

Then we to make a Procfile in the root folder of the back end app.\

release: npx sequelize db:migrate  
release: npm install -g sequelize-cli forever  
release: npm i  
web: node app.js

In this example, migrations are run, then packages are installed and the app is restarted.

Once you push your code in the branch you specified in the BITBUCKET_BRANCH variable, then all the commands you specified in the Procfile will run. The build steps are specified in the release: lines and the line running the web app is specified in the web: line of the Procfile .

Once the Procfile commands finished running, your app will be running.

Categories
JavaScript Nodejs

How to Use Sequelize to Manipulate Databases

Sequelize is a Node.js ORM with which has one of the most comprehensive features sets available.

It is similar to other ORMs like ActiveRecord, in that they are based on creating migrations with the Sequelize CLI, allowing you to write code to modify your database’s structure.

However, there are a few catches which someone has to be aware of. The migration functionality is not as smart as ActiveRecord. You cannot roll back database migration withour creating a down migration.

Also, migrations are not transactions, which means it may fail with a partially run migration where some parts of it failed to execute, leaving you with some changes made, but others not.

Sequelize CLI has to be installed separately from the library. You can run npm run --save-dev sequelize-cli to install it. After that, run npx sequelize model:generate to create your first model with its associated migration.

Add Model with Migration

For example, to create a User model with a Users table, run:

$ npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string

You may have to have administrator privileges in Windows to run this. This will create a firstName field, a lastName field, and an email field in the User model and when npx sequelize-cli migration is run, then a Users table will be created with the columns firstName , lastName and email .

The migration file should have this code:

'use strict';

module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return queryInterface.createTable('Users', {  
      id: {   
        allowNull: false,  
        autoIncrement: true,  
        primaryKey: true,  
        type: Sequelize.INTEGER  
      },  
      firstName: {  
        type: Sequelize.STRING  
      },  
      email: {  
        type: Sequelize.STRING  
      },  
    });  
   }, down: (queryInterface, Sequelize) => {  
     return queryInterface.dropTable('Users');  
   }  
};

Note the id column is created automatically, and there is a down migration in the down function where the reverse of the up migration is included. If the code in the down function is not included, then you cannot run npx sequelize-cli db:migrate:undo to undo your migration.

You can also create migration and model files separately. They will be linked together if they are named in the correct pattern. Table name should be plural of the model name. For example Users table will map to the User model. To create migration without its associated mode, run npx sequelize migration:generate .

If you have multiple operations, you have to wrap them in an array and pass the array of operations into Promise.all and return that, since the return value of the up and down functions is a promise.

Adding Constraints

Adding constraints is simple. To do this, put the following in the up function of your migration file.

queryInterface.addConstraint(  
  "Users",  
  \["email"\],  
  {  
    type: "unique",  
    name: "emailUnique"  
})

To drop this, put:

queryInterface.removeConstraint(  
  'Users',  
  'emailUnique'  
)

Associations

To make has one, has many or many to many relations between tables, you can specify that using the Model.associate function. For example, if you have a Tweets table where multiple Tweets belong to one User , you can do:

Tweet.associate = function (models) { Tweet.belongsTo(models.User, {  
    foreignKey: 'userId',  
    targetKey: 'id'  
  });  
};

foreignKey is the ID referencing the external table and targetKey is the ID column of the table you’re referencing.

And in the User model:

User.associate = function (models) {  
  User.hasMany(models.Tweet, {  
    foreignKey: 'userId',  
    sourceKey: 'id'  
  });  
};

foreignKey is the ID referencing the current table in this case and sourceKey is the ID column of the table you’re referencing.

This specifies that each User has many Tweets.

Similarly, you can replace hasMany with hasOne to specifiy one to one relationship.

To make a many to many relationship, you need a join table between the 2 tables that you want to create relationship with, then you can use belongsToMany function of your model to create the relationship. You need this in both of your tables that you are creating the relationship with.

For example if multiple Users can belong in multiple ChatRooms , then do:

User.associate = function(models) {        
  User.belongsToMany(models.ChatRoom, {      
    through: 'UserChatRooms',      
    as: 'chatrooms',      
    foreignKey: 'userId',      
    otherKey: 'chatRoomId'    
  });  
};

And for the ChatRoom model:

ChatRoom.associate = function(models) {        
  ChatRoom.belongsToMany(models.User, {      
    through: 'UserChatRooms',      
    as: 'users',      
    foreignKey: 'chatRoomId',      
    otherKey: 'userId'    
  });  
};

foreingKey is the ID that the other table references, otherKey is the key that is in the current table.

Changing Columns

You can rename a column like this:

queryInterface.renameColumn('Tweets', 'content', 'contents')

The first argument is the table name, second is the original column, third one is the new column name.

Changing data type is simple:

queryInterface.changeColumn(   
  'Tweets',  
  'scheduleDate', {  
    type: Sequelize.STRING  
  }  
)

If you want to change string to date or time, do:

queryInterface.changeColumn(  
  'Tweets',   
  'scheduleDate', {  
    type: 'DATE USING CAST("scheduleDate" as DATE)'  
  }  
)queryInterface.changeColumn(  
  'Tweets',  
  'scheduleTime', {  
     type: 'TIME USING CAST("scheduleTime" as TIME)'  
  }  
)

To run migrations, run npx sequelize db:migrate .

And there we have it — a brief look into how to use Sequelize to manipulate databases! 🎉