Categories
JavaScript Nodejs

How To Send Tweets From Your App With the Twitter API

Using the twit module to send tweets and get tweet history

If you’ve followed the steps to build the Twitter sign in to your app, you can sign in to your Twitter account and get the OAuth request token and secret.

Now we can use it to do a lot of great things, like sending tweets and getting the messages you tweeted in the past.

The back end is built with Express and the front end is built with Angular. We built the basic parts of our Twitter app in that tutorial. It has sign up, log in, and Twitter sign-in functionality.

Now, we need to add Twitter functionality so the app is useful.

To achieve this functionality, we can use the twit Node.js module. We download it by running npm i twit.

As Twitter tweets have a 280 character limit, we also need to use a link shortener to shrink our links. We do that with the Bitly Node package, which we add by running npm i bitly.

As our existing app uses Sequelize as our ORM, we can run database migrations to change the structure of our database. Here’s an article on how to use Sequelize including making models and table relations.

To make things easy, if you’re continuing from the app you built before, we remove a few things and start over. If you’re in production, you have to make migrations and add or change to achieve the same database structure.

We make a migration and add the following:

'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userName: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
hashedPassword: {
type: Sequelize.STRING
},
hashedOauthRequestToken: {
type: Sequelize.STRING
},
hashOauthRequestTokenSecret: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
}
};

Make another migration and add:

'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Tweets', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
content: {
type: Sequelize.TEXT
},
usersId: {
type: Sequelize.INTEGER
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Tweets');
}
};

Then, in the models, which should exist if we follow the tutorial on how to build the app with Twitter sign in, we put this in models/users.js:

'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
userName: DataTypes.STRING,
email: DataTypes.STRING,
hashedPassword: DataTypes.STRING,
hashedOauthAccessToken: DataTypes.STRING,
hashedOauthAccessTokenSecret: DataTypes.STRING,
activated: DataTypes.BOOLEAN,
passwordResetToken: DataTypes.STRING
}, {});
User.associate = function (models) {
User.hasMany(models.Tweet, {
foreignKey: 'userId',
sourceKey: 'id'
});
};
return User;
};

And put the following in tweet.js:

'use strict';
module.exports = (sequelize, DataTypes) => {
const Tweet = sequelize.define('Tweet', {
content: DataTypes.TEXT,
userId: DataTypes.INTEGER,
scheduled: DataTypes.BOOLEAN
}, {});
Tweet.associate = function (models) {
Tweet.hasMany(models.TweetRecord, {
foreignKey: 'tweetId',
sourceKey: 'id'
});
  Tweet.belongsTo(models.User, {
foreignKey: 'userId',
targetKey: 'id'
});
};
  return Tweet;
};

We have the models and the database structure in place to build our app after running npx sequelize db:migrate.

We make a new controller to store the Twitter tweet logic. Create a file called tweetsController.js and put in the following:

const express = require('express');
const models = require('../models');
const jwt = require('jsonwebtoken');
const Twit = require('twit');
const Op = require('sequelize').Op;
const CryptoJS = require("crypto-js");
const shortUrl = require('node-url-shortener');
const router = express.Router();
import { authCheck } from '../middlewares/authCheck';
import { saveTweetRecord, saveTweet } from '../services/tweetService';
import { BitlyClient } from 'bitly';
const bitly = new BitlyClient(process.env.BITLY_TOKEN, {});
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
router.get('/get/:id', authCheck, async (req, res) => {
try {
const id = req.params.id;
const tweets = await models.Tweet.findAll({ where: { id } });
return res.send(tweets[0]);
}
catch (ex) {
return res.send(ex, 500);
}
});
router.get('/list', authCheck, async (req, res) => {
const page = req.params.page;
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const tweets = await models.Tweet.findAll({
where: {
userId,
[Op.or]: [
{ deleted: false },
{ deleted: null }
]
}
});
res.send({
tweets,
count: tweets.length
});
});
router.post('/post', authCheck, async (req, res) => {
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
try {
let content = req.body.content;
let urls = content.match(urlRegex) || [];
urls = urls.filter(u => u && u.startsWith('http') && !u.includes('bit.ly'));
for await (const u of urls) {
const urlResult = await bitly.shorten(u);
content = content.replace(u, urlResult.url);
}
await models.Tweet.create({
...req.body,
content,
userId
})
return res.send({ message: 'Successfully created tweet' });
}
catch (ex) {
return res.send(ex, 500);
}
});
router.put('/update/:tweetId', authCheck, async (req, res) => {
try {
const token = req.headers.authorization;
const tweetId = req.params.tweetId;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
let content = req.body.content;
let urls = content.match(urlRegex) || [];
urls = urls.filter(u => u && u.startsWith('http') && !u.includes('bit.ly'));
for await (const u of urls) {
const urlResult = await bitly.shorten(u);
content = content.replace(u, urlResult.url);
}
await models.Tweet.update({
...req.body,
content,
userId
}, {
where: {
id: tweetId
}
})
return res.send({ message: 'Successfully updated tweet' });
}
catch (ex) {
console.log(ex);
return res.send(ex, 500);
}
});
router.delete('/delete/:id', authCheck, async (req, res) => {
const id = req.params.id;
try {
await models.Tweet.update(
{
deleted: true
}, {
where: { id }
}
)
return res.send({ message: 'Successfully deleted tweet' });
}
catch (ex) {
return res.send(ex, 500);
}
});
router.post('/sendTweet/:id', authCheck, async (req, res) => {
const id = req.params.id;
const token = req.headers.authorization;
try {
const tweets = await models.Tweet.findAll({
where: { id }
})
const tweet = tweets[0];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const users = await models.User.findAll({
where: { id: userId }
})
const user = users[0];
const accessTokenBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessToken, process.env.OAUTH_SECRET);
const oauthAccessToken = accessTokenBytes.toString(CryptoJS.enc.Utf8);
const accessTokenSecretBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessTokenSecret, process.env.OAUTH_SECRET);
const oauthAccessTokenSecret = accessTokenSecretBytes.toString(CryptoJS.enc.Utf8);
const T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: oauthAccessToken,
access_token_secret: oauthAccessTokenSecret,
timeout_ms: 60 * 1000,
strictSSL: true
})
T.post('statuses/update', { status: tweet.content }, (err, data, response) => {
saveTweetRecord(id);
res.send({ message: 'Successfully sent tweet' });
})
}
catch (ex) {
return res.send(ex, 500);
}
});
router.get('/importOldTweets', authCheck, async (req, res) => {
const token = req.headers.authorization;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const users = await models.User.findAll({
where: { id: userId }
})
const user = users[0];
const accessTokenBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessToken, process.env.OAUTH_SECRET);
const oauthAccessToken = accessTokenBytes.toString(CryptoJS.enc.Utf8);
const accessTokenSecretBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessTokenSecret, process.env.OAUTH_SECRET);
const oauthAccessTokenSecret = accessTokenSecretBytes.toString(CryptoJS.enc.Utf8);
         const T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: oauthAccessToken,
access_token_secret: oauthAccessTokenSecret,
timeout_ms: 60 * 1000,
strictSSL: true
})
T.get('statuses/user_timeline', {}, (err, data, response) =>
res.send({ message: 'Successfully imported tweets' });
})
}
catch (ex) {
return res.send(ex, 500);
}
});
module.exports = router;

The ’/get/:id’ route is where we get the tweets stored in our local database. findAll is the function for getting items from a table with Sequelize.

The ‘/post’ route is where we send our tweets to our Twitter account via the Twitter API.

We use the authCheck in our previous tutorial to verify the token before allowing users to send tweets. Then, we get the token from the authorization header from the front end. Then, we get the user ID.

In the try block, we shorten our links through Bit.ly. To use the bitly module, we have to sign up for a bit.ly account and get an API token. Then, we shorten everything that isn’t a Bit.ly URL with the Bit.ly API.

We can update existing tweets with the ’/update/:tweetId’ route. It is very similar to the post route except that you’re retrieving an existing tweet with the tweetId , and updating the tweet in our database.

The '/sendTweet/:id’ is where tweeting magic happens. We use the twit module which talks to the Twitter API to send the tweet.

The delete route does soft delete in case we need the data back. importOldTweets route gets our past tweet history and saves it to our database.

In this block:

const accessTokenBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessToken, process.env.OAUTH_SECRET);
const oauthAccessToken = accessTokenBytes.toString(CryptoJS.enc.Utf8);
const accessTokenSecretBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessTokenSecret, process.env.OAUTH_SECRET);
const oauthAccessTokenSecret = accessTokenSecretBytes.toString(CryptoJS.enc.Utf8);

We get the encrypted OAuth access token and secret which identifies your Twitter account and in the code below we instantiate our Twitter client:

const T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: oauthAccessToken,
access_token_secret: oauthAccessTokenSecret,
timeout_ms: 60 * 1000,
strictSSL: true
})

Then in:

T.post('statuses/update', { status: tweet.content }, (err, data, response) => {
res.send({ message: 'Successfully sent tweet' });
})

We send the tweet content to our Twitter account, letting the world see what you tweeted.

To hash encrypt our OAuth access token and secret, we replace the saveAccessTokens controller with the following:

router.get('/saveAccessTokens', authCheck, (req, res) => {
  consumer.getOAuthAccessToken(
req.query.oauth_token,
req.session.oauthRequestTokenSecret,
req.query.oauth_verifier,
(error, oauthAccessToken, oauthAccessTokenSecret, results) => {
if (error) {
logger.error(error);
res.send(error, 500);
}
else {
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const hashedOauthAccessToken = CryptoJS.AES
.encrypt(oauthAccessToken, process.env.OAUTH_SECRET)
.toString();
const hashedOauthAccessTokenSecret = CryptoJS.AES
.encrypt(oauthAccessTokenSecret, process.env.OAUTH_SECRET)
.toString();
models.User.update({
hashedOauthAccessToken,
hashedOauthAccessTokenSecret
},
{ where: { id: userId } })
.then((result) => {
return res.send({ message: 'token saved' });
})
.catch((ex) => {
logger.error(ex);
return res.send({ error: ex }, 500);
})
}
});
});

In app.js, we have to add two lines to access our routes that we added:

const tweets = require('./controllers/tweetsController');

Then add:

app.use('/tweets', tweets);

With your other app.use lines.

In the previous tutorial, we didn’t encrypt the confidential access tokens. But, if we want to make a secure app, we must do it. This saves the encrypted credentials instead of the raw ones.

In our Angular app, which we built previously, we have to add one page to see our front-end app.

We need to store our tweets in the store for easy, centralized access. We make a file called tweets-reducer.ts in our reducers folder, then we put the following in:

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

Then, in reducers/index.ts, we put:

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

In the imports array of app.module.ts, make sure we have the following added:

MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatInputModule,
MatMenuModule,
MatSidenavModule,
MatToolbarModule,
MatTableModule,
FormsModule,
HttpClientModule,
MatDialogModule,
MatDatepickerModule,
MatMomentDateModule,
MatSelectModule,
MatCardModule

We run ng g component tweetForm. This is the dialog for entering tweets. In the newly generated tweet-form.component.ts, we add:

import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { TweetsService } from '../tweets.service';
import { SET_TWEETS } from '../reducers/tweets-reducer';
import { NgForm } from '@angular/forms';
import * as moment from 'moment';
@Component({
selector: 'app-tweet-form',
templateUrl: './tweet-form.component.html',
styleUrls: ['./tweet-form.component.scss']
})
export class TweetFormComponent implements OnInit {
tweetData: any = {};
edit: boolean;
  constructor(
public dialogRef: MatDialogRef<TweetFormComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private tweetsService: TweetsService,
private store: Store<any>
) { }
  ngOnInit() {
if (this.data) {
this.edit = this.data.edit;
this.tweetData = this.data.tweet;
}
}
  getTweets(page: number = 1) {
this.tweetsService.listTweets(page)
.subscribe(res => {
this.store.dispatch({ type: SET_TWEETS, payload: res });
this.dialogRef.close();
})
}
  save(tweetForm: NgForm) {
if (tweetForm.invalid) {
return;
}
const data = Object.assign({}, this.tweetData);
if (this.tweetData.scheduleDate) {
data.scheduleDate = moment(this.tweetData.scheduleDate).format('YYYY-MM-DD');
}
if (!this.edit) {
this.tweetsService.postTweet(data)
.subscribe(res => {
this.getTweets();
})
}
else {
this.tweetsService.updateTweet(this.tweetData.id, data)
.subscribe(res => {
this.getTweets();
})
}
}
}

In tweet-form.component.html, we have:

<h2 mat-dialog-title>{{data.edit ? 'Edit' :'Add'}} Tweet</h2>
<mat-dialog-content>
<form #tweetForm='ngForm' (ngSubmit)='save(tweetForm)'>
<mat-form-field>
<input matInput placeholder="Content" required #content='ngModel' name='content'
[(ngModel)]='tweetData.content' maxlength="140">
<mat-error *ngIf="content.invalid && (content.dirty || content.touched)">
<div *ngIf="content.errors.required">
Content is required.
</div>
</mat-error>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button type='submit' (click)='save(tweetForm)'>Save</button>
</mat-dialog-actions>

The dialog will update the tweets store after saving is successful. It is used for both adding and editing tweets.

Add an entryComponents field to AppModule in app.module.ts. We add the following:

TweetFormComponent

This allows our module to use the dialog that we just created.

We have to make a page to see a table of tweets. We run ng g component tweetsPage.

Then, in the newly created tweets-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { MatDialogRef, MatDialog } from '@angular/material';
import { TweetFormComponent } from '../tweet-form/tweet-form.component';
import { TweetsService } from '../tweets.service';
import { Store, select } from '@ngrx/store';
import { SET_TWEETS } from '../reducers/tweets-reducer';
import { TweetHistoryDialogComponent } from '../tweet-history-dialog/tweet-history-dialog.component';
import { SessionService } from '../session.service';
@Component({
selector: 'app-tweets-page',
templateUrl: './tweets-page.component.html',
styleUrls: ['./tweets-page.component.scss']
})
export class TweetsPageComponent implements OnInit {
tweets: any[] = [];
displayedColumns: string[] = [
'content',
'edit',
'tweet',
'delete'
];
currentTwitterUser = {};
  constructor(
public dialog: MatDialog,
private tweetsService: TweetsService,
private store: Store<any>,
private sessionService: SessionService
) {
store.pipe(select('tweets'))
.subscribe(tweets => {
if (!tweets || !Array.isArray(tweets.tweets)) {
return;
}
this.tweets = tweets.tweets;
})
}
  ngOnInit() {
this.getTweets();
this.getCurrentTwitterAccount();
}
  openDialog(edit: boolean, index: number = null) {
const dialogRef = this.dialog.open(TweetFormComponent, {
width: '70vw',
data: {
edit,
tweet: edit ? this.tweets[index] : {}
}
})
  dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
  getTweets() {
this.tweetsService.listTweets()
.subscribe(res => {
this.store.dispatch({ type: SET_TWEETS, payload: res });
})
}
  deleteTweet(id: number) {
this.tweetsService.deleteTweet(id)
.subscribe(res => {
this.getTweets();
})
}
  sendTweet(id: number) {
this.tweetsService.sendTweet(id)
.subscribe(res => {
alert('Tweet sent');
})
}
  openTweetHistory(index: number) {
const dialogRef = this.dialog.open(TweetHistoryDialogComponent, {
width: '70vw',
data: this.tweets[index] || {}
})
    dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
  getCurrentTwitterAccount() {
this.sessionService.getCurrentTwitterAccount()
.subscribe(res => {
this.currentTwitterUser = res;
})
}
  importOldTweets() {
this.tweetsService.importOldTweets()
.subscribe(res => {
this.getTweets();
alert('Tweets successfully imported.');
})
}
}

To add a table to display our tweets, we put the following in tweet-page.component.html:

<div class="center">
<h1>Tweets</h1>
</div>
<br>
<button mat-raised-button (click)='openDialog(false)'>Add Tweet</button>
<button mat-raised-button (click)='importOldTweets()'>Import Old Tweets</button>
<div id='table'>
<table mat-table [dataSource]="tweets" class="mat-elevation-z8">
       <ng-container matColumnDef="content">
<th mat-header-cell *matHeaderCellDef>
Content
</th>
<td mat-cell *matCellDef="let element">{{element.content}}</td>
</ng-container>
       <ng-container matColumnDef="edit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element; let i = index">
<button mat-raised-button (click)='openDialog(true, i)'>Edit</button>
</td>
</ng-container>
        <ng-container matColumnDef="tweet">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button (click)='sendTweet(element.id)' [disabled]='!currentTwitterUser.id'>
Tweet
</button>
</td>
</ng-container>
        <ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button (click)='deleteTweet(element.id)'>Delete</button>
</td>
</ng-container>
        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>

We add a service for making the HTTP requests. We run ng g service tweets to make tweets.service.ts.

We put in:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class TweetsService {
  constructor(
private http: HttpClient
) { }
  getTweets(id: number) {
return this.http.get(`${environment.apiUrl}/tweets/get/${id}`)
}
  listTweets() {
return this.http.get(`${environment.apiUrl}/tweets/list`)
}
  postTweet(data) {
return this.http.post(`${environment.apiUrl}/tweets/post`, data)
}
  updateTweet(id, data) {
return this.http.put(`${environment.apiUrl}/tweets/update/${id}`, data)
}
  deleteTweet(id: number) {
return this.http.delete(`${environment.apiUrl}/tweets/delete/${id}`)
}
  sendTweet(id: number) {
return this.http.post(`${environment.apiUrl}/tweets/sendTweet/${id}`, {})
}
  getTweetRecord(tweetId: number) {
return this.http.get(`${environment.apiUrl}/tweets/tweetRecord/${tweetId}`)
}
  importOldTweets() {
return this.http.get(`${environment.apiUrl}/tweets/importOldTweets`)
}
}

In the end, we have:

Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

Categories
JavaScript Nodejs

How To Create a Simple Front End With Authenticated Routes

Creating pages that require authentication is simple in Angular. A guard is a piece of Angular code that is designed to control access to routes.

In app-routing.module.ts, we have an array of Route objects and in each entry, you can add a canActivate property where you can pass an array of guards.

The guard allows you to check for the requirements to access your routes. You return true, or a promise that resolves to true, to allow people to access your route. Otherwise, return false or return your promise to false.

In this story, we use the API we wrote in a previous piece.

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 to make our UI look pretty.

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/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 our dependencies and components that we’ll add. 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 state centrally in our flux store and in that file we enter:

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 };

In index.ts of the same folder, we put:

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

To link our reducer to other parts of the app.

In style.css, to get our Material Design look, we put:

/* 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;  
}

In index.html, we add the following in between the head tags:

<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. That 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)';  
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 an 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 accessing 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 back to home page.

Now, we create the forms for logging in setting the user data after logging in.

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.ts generated 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({  
  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.

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';  
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']);  
      })  
  }  
}

The 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 passwords 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 nav 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 outside the side nav. If we click outside, i.e. we’re 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

How To Make a Simple Back End With User Accounts and Authentication

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 checking for its authenticity. They do this by checking against a secret key stored in the back end and also by checking for an expiry date.

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

In this piece, we will build an app that uses JWT to store authentication data.


Overview

For the back end, we’ll use the Express framework, which runs on Node.js, and for the front end, we’ll use the Angular framework. Both have their own JWT add-ons. On the back end, we have the jsonwebtoken package for generating and verify the token.

On the front end, we have 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, 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’ll 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 as valid against our secret key. There will also be a sign-up and user credential settings pages, in addition to a login page.


Building the App

With this plan, we can begin.

First, we create 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 have 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 key.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. First, we run npx sequelize-cli init to generate 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 two 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 .

Then we write some code. First, we put the following in app.js :

require("[@babel/register](http://twitter.com/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 the 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 { saltRounds } from '../exports';  
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, saltRounds)  
        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. If it’s found, it then checks for the hashed password with the compare function of bcrypt. If both are successful, then a JWT is generated. The signup route gets the 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.

This first is the plain text password, and the second is a number of salt rounds.

updatePassword route is an authenticated route. It checks for the token, and if it’s valid, it will continue to save the user’s password by searching for the User with the user ID from the decoded token.

We will add the authCheck middleware next. We 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);  
    }  
}

You should use the same process.env.JWT_SECRET for generating and verifying the token. Otherwise, verification will fail. The secret shouldn’t be shared anywhere and shouldn’t be checked in to version control.

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

We make an .env file of the root of the back-end app folder, with the following content. (This shouldn’t be checked in to version control.)

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 can we can use a front-end app, mobile app, or any HTTP client to sign in.

Categories
JavaScript Nodejs

How To Build a Reverse Proxy With Express

If you use microservices architecture for your distributed system, there is a need for an API gateway. One good way to build an API gateway is to use a reverse proxy. Node.js is good for a reverse proxy because it’s fast and has lots of libraries to make a reverse proxy.

Express is the most popular framework for building web apps. It’s also very good for building a reverse proxy because there are add-ons to Express that can do the routing for the reverse proxy.

The design will be very simple. It will only redirect traffic to our different back-end apps according to the URL of the request. Also, it has to be able to pass header data, files, and cookies into our services in addition to the request payload.

To do all this, we first scaffold our app by running the Express application generator by following the instructions at https://expressjs.com/en/starter/generator.html

We have to run npx express-generator to generate the code that we need.

I suggest using nodemon to run our app in a development environment so that it restarts whenever our code changes.

Next, we have to install some packages to do the reverse proxy function and to enable CORS so that front end apps can use the reverse proxy. To do this, run npm i express-http-proxy glob node-env-file cookie-parser babel-register body-parser .

express-http-proxy is the HTTP reverse proxy library. cors is an add-on for Express that enables CORS. cookie-parser is an add-on allows Express to parse cookies. babel-register allows us to use the latest JavaScript features. node-env-file allows us to use .env file for storing environment variables. body-parser will be used to check for multipart requests. Multipart requests are requests that have files. The requests with files will not go through body-parser before redirecting.

Now we are ready to write code. We make a new file called helper.js in the helpers folder that we create.

In there, we add:

module.exports = {  
    isMultipartRequest: (req) => {  
        let contentTypeHeader = req.headers['content-type'];  
        return contentTypeHeader && contentTypeHeader.indexOf('multipart') > -1;  
    }  
}

This function checks for multipart requests, which are requests that are sent in as form data. It can include text or files.

In app.js , we write:

require("babel-register");  
let express = require('express');  
let cors = require('cors')  
let config = require('./config/config');  
let env = require('node-env-file');  
let helpers = require('./app/helpers/helpers');  
let bodyParser = require('body-parser');  
env(__dirname + '/.env');
let app = express();

app.use(cors({  
  credentials: true,  
  origin: true  
}));

const bodyParserJsonMiddleware = function () {  
  return function (req, res, next) {  
    if (helpers.isMultipartRequest(req)) {  
      return next();  
    }  
    return bodyParser.json()(req, res, next);  
  };  
};

app.use(bodyParserJsonMiddleware());

app.all('*', (req, res, next) => {  
  let origin = req.get('origin');  
  res.header('Access-Control-Allow-Origin', origin);  
  res.header("Access-Control-Allow-Headers", "X-Requested-With");  
  res.header('Access-Control-Allow-Headers', 'Content-Type');  
  next();  
});

module.exports = require('./config/express')(app, config);

app.listen(config.port, () => {  
  console.log('Express server listening on port ' + config.port);  
});

The code works by passing on server-side cookies and allows multipart requests to not pass through body-parser since it is not JSON.

Also, we allow CORS in the block below:

app.all('*', (req, res, next) => {  
  let origin = req.get('origin');  
  res.header('Access-Control-Allow-Origin', origin);  
  res.header("Access-Control-Allow-Headers", "X-Requested-With");  
  res.header('Access-Control-Allow-Headers', 'Content-Type');  
  next();  
});

It allows requests from all origins to be accepted. By default, only requests from the same host as the back end are accepted since it is insecure to allow requests from other hosts. However, if we allow mobile and standalone front-end web apps to make requests through our reverse proxy, we have to allow all origins. It gets the origin from the header and allows the request from that origin to proceed.

Then we add the code for proxying requests in controllers/home.js:

const express = require('express');  
let proxy = require("express-http-proxy");  
let helpers = require('../helpers/helpers');
let loginAppRoutes =[  
  '/login*',  
  '/loginms',  
  '/register',  
  '/resetPassword',  
  '/getActivationKey*',  
  '/activateAccount',  
  '/logout',  
  '/reports',  
  '/'  
]

let uploadRoutes = [  
  '/uploadlogo*',  
]

module.exports = (app) => {  
  const proxyMiddleware = () => {  
    return (req, res, next) => {  
      let reqAsBuffer = false;  
      let reqBodyEncoding = true;  
      let contentTypeHeader = req.headers['content-type'];  
      if (helpers.isMultipartRequest(req)) {  
        reqAsBuffer = true;  
        reqBodyEncoding = null;  
      }  
      return proxy(process.env.UPLOAD_URL, {  
        reqAsBuffer: reqAsBuffer,  
        reqBodyEncoding: reqBodyEncoding,  
        parseReqBody: false,  
        proxyReqOptDecorator: (proxyReq) => {  
          return proxyReq;  
        }, 

        proxyReqPathResolver: (req) => {  
          return `${process.env.UPLOAD_APP_URL}/${req.baseUrl}${req.url.slice(1)}`;  
        }, 

        userResDecorator: (rsp, data, req, res) => {  
          res.set('Access-Control-Allow-Origin', req.headers.origin);  
          return data.toString('utf-8');  
        }  
      })(req, res, next);  
    };  
  } 

  uploadRoutes.forEach(r => {  
    app.use(r, proxyMiddleware());  
  }) 

  loginAppRoutes.forEach(r => {  
    app.use(r, proxy(process.env.LOGIN_URL, {  
      proxyReqOptDecorator: (proxyReq) => {  
        return proxyReq;  
      },

      proxyReqPathResolver: (req) => {  
        return `${process.env.LOGIN_URL}/${req.baseUrl}${req.url.slice(1)}`;  
      }, 

      userResDecorator: (rsp, data, req, res) => {  
        res.set('Access-Control-Allow-Origin', req.headers.origin);  
        return data.toString('utf-8');  
      }  
    }));  
  })  
};

We check for multipart form requests with:

if (helpers.isMultipartRequest(req)) {  
  reqAsBuffer = true;  
  reqBodyEncoding = null;  
}

The will pass requests with files straight through to our internal APIs.

The following:

proxyReqPathResolver: (req) => {  
  return `${process.env.LOGIN_URL}/${req.baseUrl}${req.url.slice(1)}`;  
},

is where the actual redirect URL from the proxy to internal API is set. Note that parseReqBody is false , so that multipart form requests are not parsed as JSON.

The process.env variables are set in the .env file.

In the .env file, we have:

LOGIN_URL='http://urlForYourLoginApp'  
UPLOAD_URL='http://anotherUrlForYourUploadApp'

With reverse proxy add-on, Express is one of the best choices for building a reverse proxy in Node.

Categories
JavaScript Nodejs React

How to Create Word Documents with Node.js

Microsoft Word is a very popular word processor. It is a popular format for making and exchanging documents. Therefore, a feature of a lot of apps is to create Word documents from other kinds of documents. Creating Word documents is easy in Node.js apps with third-party libraries. We can make an app from it easily ourselves.

In this article, we will make an app that lets users enter their document in a rich text editor and generate a Word document from it. We will use Express for back end and React for front end.

Back End

We will start with the back end. To start, we will create a project folder with the backend folder inside. Then in the backend folder run npx express-generator to create the Express app. Then run npm i to install the packages Next we install our own packages. We need Babel for run the app with the latest version of JavaScript, CORS for cross domain requests with front end, HTML-DOCX-JS for converting HTML strings to Word documents, Multer for file upload, Sequelize for ORM, and SQLite3 for our database.

We install all of these by running npm i @babel/cli @babel/core @babel/node @babel/preset-env cors html-docx-js sequelize sqlite3 multer .

After that we change the scripts section of package.json to have:

"start": "nodemon --exec npm run babel-node --  ./bin/www",  
"babel-node": "babel-node"

so that we run our app with Babel instead of the regular Node runtime.

Then create a .babelrc file in the backend folder and add:

{
    "presets": [
        "@babel/preset-env"
    ]
}

to specify that we run our app with the latest version of JavaScript.

Next we add our database code. Run npx sequelize-cli init in the backend folder to create the Sequelize code.

We should have a config.js in the project now. In there, add:

{  
  "development": {  
    "dialect": "sqlite",  
    "storage": "development.db"  
  },  
  "test": {  
    "dialect": "sqlite",  
    "storage": "test.db"  
  },  
  "production": {  
    "dialect": "sqlite",  
    "storage": "production.db"  
  }  
}

to specify that we SQLite for our database.

Next create our model and migration by running:

npx sequelize-cli model:create --name Document --attributes name:string,document:text,documentPath:string

to create a Document model and Documents table.

We then run:

npx sequelize-cli db:migrate

to create the database.

Next we create our routes. Create a document.js file in the routes folder and add:

var express = require("express");
const models = require("../models");
var multer = require("multer");
const fs = require("fs");
var router = express.Router();
const htmlDocx = require("html-docx-js");
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./files");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});
const upload = multer({
  storage
});
router.get("/", async (req, res, next) => {
  const documents = await models.Document.findAll();
  res.json(documents);
});
router.post("/", async (req, res, next) => {
  const document = await models.Document.create(req.body);
  res.json(document);
});
router.put("/:id", async (req, res, next) => {
  const id = req.params.id;
  const { name, document } = req.body;
  const doc = await models.Document.update(
    { name, document },
    { where: { id } }
  );
  res.json(doc);
});
router.delete("/:id", async (req, res, next) => {
  const id = req.params.id;
  await models.Document.destroy({ where: { id } });
  res.json({});
});
router.get("/generate/:id", async (req, res, next) => {
  const id = req.params.id;
  const documents = await models.Document.findAll({ where: { id } });
  const document = documents[0];
  const converted = htmlDocx.asBlob(document.document);
  const fileName = `${+new Date()}.docx`;
  const documentPath = `${__dirname}/../files/${fileName}`;
  await new Promise((resolve, reject) => {
    fs.writeFile(documentPath, converted, err => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
  const doc = await models.Document.update(
    { documentPath: fileName },
    { where: { id } }
  );
  res.json(doc);
});
router.post("/uploadImage", upload.single("upload"), async (req, res, next) => {
  res.json({
    uploaded: true,
    url: `${process.env.BASE_URL}/${req.file.filename}`
  });
});
module.exports = router;

We do the standard CRUD operations to the Documents table in the first 4 routes. We have GET for getting all the Documents , POST for create a Document from post parameters, PUT for updating Document by ID, DELETE for deleting a Document by looking it up by ID. We have the HTML in the document field for generating the Word document later.

The generate route is for generating the Word document. We get the ID from the URL and then use the HTML-DOCX-JS package to generate a Word document. We generate the Word document by converting the HTML document to a file stream object with the HTML-DOCX-JS package and then writing the stream to a file and saving the path to the file in the Document entry in with the ID in the URL parameter.

We also have a uploadImage route to let user upload images with CKEditor with the CKFinder plugin. The plugin expects uploaded and url in the response so we return those.

Then we need to add a files folder in the backend folder.

Next in app.js , we replace the existing code with:

require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var cors = require("cors");
var indexRouter = require("./routes/index");
var documentRouter = require("./routes/document");
var app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(express.static(path.join(__dirname, "files")));
app.use(cors());
app.use("/", indexRouter);
app.use("/document", documentRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
  res.status(err.status || 500);
  res.render("error");
});
module.exports = app;

We expose the file folder with:

app.use(express.static(path.join(__dirname, "files")));

and expose the document route with:

var documentRouter = require("./routes/document");  
app.use("/document", documentRouter);

Front End

Now the back end is done, we can move onto the front end. Create the React app by running Create React App. We run npx create-react-app frontend in the root folder of the project.

We then install our packages. We will use CKEditor for our rich text editor, Axios for making HTTP requests, Bootstrap for styling, MobX for simple state management, React Router for routing URLs to components, and Formik and Yup for form value handling and form validation respectively.

Install all the packages by running npm i @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-react axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup .

With the packages installed, we can get started. In App.js , we replace the existing code with:

import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import TopBar from "./TopBar";
import { DocumentStore } from "./store";
import "./App.css";
const history = createHistory();
const documentStore = new DocumentStore();
function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} documentStore={documentStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;

to add our top bar and route to the home page.

In App.css , we replace the existing code with:

.page {
  padding: 20px;
}
.content-invalid-feedback {
  width: 100%;
  margin-top: 0.25rem;
  font-size: 80%;
  color: #dc3545;
}
nav.navbar {
  background-color: green !important;
}

to add some padding to our page and style the validation message for the Rich text editor, and change the color of the navbar.

Next we create the form for adding and editing documents. Create a DocumentForm.js in the src file and add:

import React from "react";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import { Formik, Field } from "formik";
import { addDocument, editDocument, getDocuments, APIURL } from "./request";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
const schema = yup.object({
  name: yup.string().required("Name is required")
});
function DocumentForm({ documentStore, edit, onSave, doc }) {
  const [content, setContent] = React.useState("");
  const [dirty, setDirty] = React.useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid || !content) {
      return;
    }
    const data = { ...evt, document: content };
    if (!edit) {
      await addDocument(data);
    } else {
      await editDocument(data);
    }
    getAllDocuments();
  };
  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    onSave();
  };
  return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? doc : {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="name">
                <Form.Label>Name</Form.Label>
                <Form.Control
                  type="text"
                  name="name"
                  placeholder="Name"
                  value={values.name || ""}
                  onChange={handleChange}
                  isInvalid={touched.name && errors.name}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.name}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="content">
                <Form.Label>Content</Form.Label>
                <CKEditor
                  editor={ClassicEditor}
                  data={content || ""}
                  onInit={editor => {
                    if (edit) {
                      setContent(doc.document);
                    }
                  }}
                  onChange={(event, editor) => {
                    const data = editor.getData();
                    setContent(data);
                    setDirty(true);
                  }}
                  config={{
                    ckfinder: {
                      uploadUrl:
                        `${APIURL}/document/uploadImage`
                    }
                  }}
                />
                <div className="content-invalid-feedback">
                  {dirty && !content ? "Content is required" : null}
                </div>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Save
            </Button>
            <Button type="button">Cancel</Button>
          </Form>
        )}
      </Formik>
    </>
  );
}
export default observer(DocumentForm);

We wrap our React Bootstrap Form inside the Formik component to get the form handling function from Formik which we use directly in the React Bootstrap form fields. We cannot do the same with CKEditor, so we write our own form handlers for the rich text editor. We set the data prop in the CKEditor to set the value of the input of the rich text editor. The onInit function is used with users try to edit an existing document since we have to set the data prop with the editor initializes by running setContent(doc.document); . The onChange prop is the handler function for setting content whenever it is updated so the data prop will have the latest value, which we will submit when the user clicks Save.

We use the CKFinder plugin to upload images. To make it work, we set the image upload URL to the URL of the upload route in our back end.

The form validation schema is provided by the Yup schema object which we create at the top of the code. We check if the name field is filled in.

The handleSubmit function is for handling submitting the data to back end. We check both the content and the evt object to check both fields since we cannot incorporate the Formik form handlers directly into the CKEditor component.

If everything is valid, then we add a new document or update it depending on on whether the edit prop is true or not.

Then when saving is successful, we call getAllDocuments to populate the latest documents into our MobX store by running documentStore.setDocuments(response.data); .

Next we make our home page by creating HomePage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import DocumentForm from "./DocumentForm";
import Modal from "react-bootstrap/Modal";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getDocuments, deleteDocument, generateDocument, APIURL } from "./request";
function HomePage({ documentStore, history }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [doc, setDoc] = useState([]);
  const openAddTemplateModal = () => {
    setOpenAddModal(true);
  };
  const closeAddModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
  };
  const cancelAddModal = () => {
    setOpenAddModal(false);
  };
  const cancelEditModal = () => {
    setOpenEditModal(false);
  };
  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    setInitialized(true);
  };
  const editDocument = d => {
    setDoc(d);
    setOpenEditModal(true);
  };
  const onSave = () => {
    cancelAddModal();
    cancelEditModal();
  };
  const deleteSingleDocument = async id => {
    await deleteDocument(id);
    getAllDocuments();
  };
  const generateSingleDocument = async id => {
    await generateDocument(id);
    alert("Document Generated");
    getAllDocuments();
  };
  useEffect(() => {
    if (!initialized) {
      getAllDocuments();
    }
  });
  return (
    <div className="page">
      <h1 className="text-center">Documents</h1>
      <ButtonToolbar onClick={openAddTemplateModal}>
        <Button variant="primary">Add Document</Button>
      </ButtonToolbar>
      <Modal show={openAddModal} onHide={closeAddModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            onSave={onSave.bind(this)}
            cancelModal={cancelAddModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <Modal show={openEditModal} onHide={cancelEditModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            edit={true}
            doc={doc}
            onSave={onSave.bind(this)}
            cancelModal={cancelEditModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>Name</th>
            <th>Document</th>
            <th>Generate Document</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {documentStore.documents.map(d => {
            return (
              <tr key={d.id}>
                <td>{d.name}</td>
                <td>
                  <a href={`${APIURL}/${d.documentPath}`} target="_blank">
                    Open
                  </a>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={generateSingleDocument.bind(this, d.id)}
                  >
                    Generate Document
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editDocument.bind(this, d)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSingleDocument.bind(this, d.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
}
export default withRouter(observer(HomePage));

We have a React Bootstrap table for listing the documents with buttons to edit, delete documents and generate Word document. Also, there is a Open link for opening the Word document in each row. We have a create button on top of the table.

When the page loads, we call getAllDocuments and populate them in the MobX store. We open and close the add and edit modals with the openAddTemplateModal, closeAddModal, cancelAddModal, cancelEditModal functions.

Next create request.js in the src folder and add:

export const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getDocuments = () => axios.get(`${APIURL}/document`);
export const addDocument = data => axios.post(`${APIURL}/document`, data);
export const editDocument = data => axios.put(`${APIURL}/document/${data.id}`, data);
export const deleteDocument = id => axios.delete(`${APIURL}/document/${id}`);
export const generateDocument = id => axios.get(`${APIURL}/document/generate/${id}`);

to add the functions to make requests to our routes in the back end.

Then we create our MobX store. Create store.js in the src folder and put:

import { observable, action, decorate } from "mobx";
class DocumentStore {
  documents = [];
setDocuments(documents) {
    this.documents = documents;
  }
}
DocumentStore = decorate(DocumentStore, {
  documents: observable,
  setDocuments: action
});
export { DocumentStore };

We have the function setDocuments to put the photo data in the store, which we used in HomePage and DocumentForm and we instantiated it before exporting so that we only have to do it in one place.

This block:

DocumentStore = decorate(DocumentStore, {  
  documents: observable,  
  setDocuments: action  
});

designates the documents array in DocumentStore as the entity that can be watched by components for changes. The setDocuments function is designated as the function that can be used to set the documents array in the store.

Next we create the top bar by creating a TopBar.js file in the src folder and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Word App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={location.pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app. We only display it with the token present in local storage. We check the pathname to highlight the right links by setting the active prop.

Next in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Word App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to add the Bootstrap CSS and change the title.

After writing all that code, we can run our app. Before running anything, install nodemon by running npm i -g nodemon so that we don’t have to restart back end ourselves when files change.

Then run back end by running npm start in the backend folder and npm start in the frontend folder, then choose ‘yes’ if you’re asked to run it from a different port.