read24 - Server: API Endpoints - Book and Login

July 3, 2020

Roger Ngo

We will mock a database. The database will just be a JSON file for now. Now, in order for the TypeScript project to recognize JSON imports, the tsconfig.json file must be edited to support this. The resolveJsonModule property should be added with true.

{
    "compilerOptions": {
        "module": "commonjs",
        "removeComments": true,
        "outDir": "dist",
        "resolveJsonModule": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}

Onto the mock database itself. It will just be a file in the filesystem called mockDb.json. Initialize this file with contents to include some basic book data.

{
    "books": [
        {
            "id": 1,
            "title": "Where the Red Fern Grows",
            "author": "Wilson Rawls",
            "year": "1974",
            "publisher": "Bantam Books",
            "genre": 0,
            "isbn": "978-0-553-12338-8",
            "lexile": 700,
            "wordCount": 75528
        },
        {
            "id": 2,
            "title": "Esperanza Rising",
            "author": "Pam Munoz Ryan",
            "year": 2000,
            "publisher": "Scholastic, Inc.",
            "genre": 0,
            "isbn": "0-439-12041-1",
            "lexile": 750,
            "wordCount": 41905
        }
    ]
}

Now that there is some data. The book routes can be implemented in book.ts.

import { IRouter } from "express";
import * as db from '../mockDb.json'

export function mountBook(app: IRouter) {
    app.get('/book/search/title/:bookTitle', (req, res) => {
        const bookTitle = req.params.bookTitle.trim().toLowerCase();
        const books = db.books;

        const results = books.filter(b => b.title.toLowerCase().indexOf(bookTitle) !== -1);
        
        res.status(200).json(results);
    });    

    app.get('/book/search/author/:authorName', (req, res) => {
        const authorName = req.params.authorName.trim().toLowerCase();
        const books = db.books;
        
        const results = books.filter(b => b.author.toLowerCase().indexOf(authorName) !== -1);
        
        res.status(200).json(results);
    });
}

For the API, all responses should be in JSON format.

In order to make sure these endpoints still work, and return the proper results given the request body, we can use Postman to send off a request.

First search by title, where the red fern grows:

Search Title

How about with esperanza?

Search Title 2

Now, let's try searching by author name. First with ryan.

Search Author

Then with wilson.

Search Author 2

Now, suppose there are no results returned:

No results

Cool, an empty result is returned instead. That's what I want.

Saving the quiz endpoints for a different discussion, let's create some logic for login. Our data must be hashed. I'll need to install the bcrypt dependency to create hashed strings, and safely store the student login credentials.

npm install bcrypt --save
npm install @types/bcrypt --save-dev

I will also need to create two utility functions which will hash passwords, and confirm whether or not user provided plain text passwords match what is stored in the DB. These will go in the login routes file, login.ts.

function hashPassword(plainTextPassword: string) {
    const saltRounds = 7;
    const salt = bcrypt.genSaltSync(saltRounds);
    const hashed = bcrypt.hashSync(plainTextPassword, salt);

    return {
        salt,
        hashed
    };
}

function isValidHash(plainTextPassword: string, hashed: string, salt: string) {
    const testHashed = bcrypt.hashSync(plainTextPassword, salt);
    
    return hashed === testHashed;
}

Then I create a basic script to call hashPassword with password as the plain text password parameter. The result will be used as mock data for the student.

{"salt":"$2b$07$uLoKxQ9vxs7jKVSZ8u7pMu","hashed":"$2b$07$uLoKxQ9vxs7jKVSZ8u7pMuQx5d0SvYZL0NkXX/S08FqHECGG2yB9u"}

Then modify the database to include entries for classrooms, users, and students.

{
    "classrooms": [
        {
            "id": 1,
            "name": "Room 4",
            "slug": "room4"
        }
    ],
    "users": [
        {
            "id": 1,
            "username": "stu01",
            "password": "$2b$07$uLoKxQ9vxs7jKVSZ8u7pMuQx5d0SvYZL0NkXX/S08FqHECGG2yB9u",
            "salt": "$2b$07$uLoKxQ9vxs7jKVSZ8u7pMu"
        }
    ],
    "students": [
        {
            "id": 1,
            "classroomId": 1,
            "firstName": "Stu",
            "middleName": "",
            "lastName": "Dent",
            "grade": 5,
            "userId": 1
        }
    ],
    "books": ...
}

Then we can implement the /login route to log the user in. For now, a 200 OK, and greeting message is sent back.

import { IRouter } from "express";
import * as bcrypt from 'bcrypt';
import * as db from '../mockDb.json';

const classrooms = db.classrooms;
const students = db.students;
const users = db.users;

function hashPassword(plainTextPassword: string) {
    const saltRounds = 7;
    const salt = bcrypt.genSaltSync(saltRounds);
    const hashed = bcrypt.hashSync(plainTextPassword, salt);

    return {
        salt,
        hashed
    };
}

function isValidHash(plainTextPassword: string, hashed: string, salt: string) {
    const testHashed = bcrypt.hashSync(plainTextPassword, salt);

    return hashed === testHashed;
}

export function mountLogin(app: IRouter) {
    app.post('/login', (req, res) => {
        const username = req.body.username;
        const password = req.body.password;

        if (!username || !password)
            return res.status(400).json({message: 'Must provide username, and password.'});

        const user = users.find(u => u.username === username);

        if (!user)
            return res.status(404).json({message: 'Could not find user by the username.'});

        if (!isValidHash(password, user.password, user.salt))
            return res.status(401).json({message: 'Invalid password.'});

        const classroom = classrooms.find(c => c.id === students.find(s => s.userId === user.id).classroomId);

        return res.status(200).json({message: `Welcome to ${classroom.name}`});
    });
}

Let's test the /login route with a happy scenario.

Request Body

{
    "username": "stu01",
    "password": "password"
}

Response Body

{
    "message": "Welcome to Room 4"
}

Nice, it is what I want.

Here are various scenarios tested:

If the student provides a valid set of login credentials:

Login 200

If the student provides an invalid password:

Login 401

If the student somehow forgets to send a password, or username:

Login 400

If the student provides an invalid username:

Login 404

This is all good! Now, let's commit.

Rogers-MacBook-Pro:server rogerngo$ git add .
Rogers-MacBook-Pro:server rogerngo$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   package-lock.json
	modified:   package.json
	new file:   src/mockDb.json
	modified:   src/routes/book.ts
	modified:   src/routes/login.ts
	modified:   tsconfig.json

Rogers-MacBook-Pro:server rogerngo$ git commit -m "Book, and login route implementations."
[master 38f9268] Book, and login route implementations.
 6 files changed, 579 insertions(+), 4 deletions(-)
 create mode 100644 server/src/mockDb.json
Rogers-MacBook-Pro:server rogerngo$ git push
Enumerating objects: 20, done.
Counting objects: 100% (20/20), done.
Delta compression using up to 8 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (11/11), 12.62 KiB | 12.62 MiB/s, done.
Total 11 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
To https://github.com/urbanspr1nter/read24.git
   bcdeaf2..38f9268  master -> master
Rogers-MacBook-Pro:server rogerngo$ 

I'm happy with coding for this session. Let's tackle the quiz endpoints next.