Dapr - Creating a User Login/Register Microservice

Xavier Geerinck

April 10, 2020 / coding coding-js dapr

In an earlier post, I went in more detail how you can utilize Dapr to create a Microservice that communicates over gRPC for quick Inter-Process Communication (IPC).

Now I would like to take this a step further and create a real microservice that allows us to perform account management.

Pre-Requisites

Note: You can update your Dapr version through one of the following

  • Linux: wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
  • PowerShell: powershell -Command "iwr -useb https://raw.githubusercontent.com/dapr/cli/master/install/install.ps1 | iex"

Project Scope / Requirements

Scope: Allow us to create users and let them login, returning a JWT token.

Routes:

  • /user/login should return a JWT token
  • /user/register should create an account

Writing a Hello World route in Express and Typescript

Since Dapr takes care of state management, we are going to write a Microservice that automatically hooks up to this state management (in this case creating and finding our users). But first we have to install our dependencies, so let’s install those:

# Dependencies Installation
npm i -S express body-parser isomorphic-fetch express-validator bcrypt uuid
npm i @types/express @types/uuid @types/bcrypt @types/isomorphic-fetch @types/express-validator --save-dev

Whereafter we can add the following code which spins up the server and initializes the route / that will respond with Hello World.

import express = require('express');

const PORT = process.env.APP_HTTP_PORT || 9000;
const PORT_DAPR = process.env.DAPR_HTTP_PORT || 9500;
const app: express.Application = express();

app.get('/', (req, res) => res.send('Hello World'));
app.listen(PORT, () => console.log(`Server started at 0.0.0.0:${PORT}, Dapr at ${PORT_DAPR}`));

Starting this with Dapr on the app port 9000 can be done with:

dapr run --app-id node-api-user --app-port 9000 --port 9500 npm run start

Note: Port 9500 will be used for calling Dapr (e.g. state management)

Dapr State Management

Setting up connection to CosmosDB

For our State Management backend we will utilize CosmosDB. To set this up we can refer to the documentation which explains that we should adapt our local created components/statestore.yaml file. When doing this, we end up with this:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state-cosmosdb
spec:
  type: state.azure.cosmosdb
  metadata:
  - name: url
    value: <YOUR_URL>
  - name: masterKey
    value: <YOUR_PRIMARY_KEY>
  - name: database
    value: <YOUR_DB_NAME>
  - name: collection
    value: <YOUR_COLLECTION_NAME>

Note: For use in production you can create a .yaml file (e.g. cosmos.yaml) and deploy it with kubectl apply -f cosmos.yaml

After doing this, we are now able to access our state through the DAPR port which we selected during start-up. We can access this through the environment variable DAPR_HTTP_PORT.

Example State Call

We can now store and receive states through http requests. To do this I create a sample.http file which includes the lines I want to send. When then installing the VSCode Extension: REST Client, I am able to utilize CTRL + ALT + R or CTRL + SHIFT + P; > Rest Client: Send Request to send a request.

./rest-client.png

My sample.http file looks like this:

POST http://localhost:9500/v1.0/state/state-cosmosdb

[
  {
    "id": "e86f44ee-ed09-4a71-afd7-3d6bbf782b39",
    "name": "Xavier Geerinck"
  }
]

###

GET http://localhost:9500/v1.0/state/state-cosmosdb/e86f44ee-ed09-4a71-afd7-3d6bbf782b39

Whereafter when Running the 2 queries we get the following results (CTRL + ALT + R):

./dapr-state-post.png

./dapr-state-get.png

Writing our API Routes with Express

We can now write our API routes with the concepts learned above. When we implement this, we get the following end-result:

import express = require('express');
import { v4 as uuidv4 } from 'uuid';
const bodyParser = require('body-parser');
const { check, validationResult } = require('express-validator');
const bcrypt = require('bcrypt');
const fetch = require('isomorphic-fetch');

const PORT = process.env.APP_HTTP_PORT || 9000;
const PORT_DAPR = process.env.DAPR_HTTP_PORT || 9500;
const DAPR_URL = `http://localhost:${PORT_DAPR}/v1.0`;
const DAPR_URL_STATE = `${DAPR_URL}/state/state-cosmosdb`;

const app: express.Application = express();

app.use(bodyParser.json());

app.listen(PORT, () => console.log(`Server started at 0.0.0.0:${PORT}, Dapr at ${PORT_DAPR}`));

app.post('/user/register', [
  check('firstName').isAlpha(),
  check('lastName').isAlpha(),
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
], async (req: express.Request, res: express.Response) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }

  try {
    // First encrypt password
    const bcryptSalt = await bcrypt.genSalt(10);
    req.body.password = await bcrypt.hash(req.body.password, bcryptSalt);

    // Save User
    const userId = uuidv4();
    console.log(`Creating user with Id: ${userId}`);
    const fetchRes = await fetch(`${DAPR_URL_STATE}`, {
      method: 'POST',
      body: JSON.stringify([{
        key: req.body.email,
        value: {
          id: userId,
          ...req.body
        }
      }]),
      headers: {
        "Content-Type": "application/json"
      }
    });

    // Handle Response
    if (!fetchRes.ok) {
      throw "Failed to register user";
    }

    return res.status(200).send({ success: true, user: {
      id: userId,
      ...req.body
    }});
  } catch (e) {
    return res.status(500).send({ message: e });
  }
});

app.post('/user/login', [
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
], async (req: express.Request, res: express.Response) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
  
  try {
    // Find existing user
    const fetchRes = await fetch(`${DAPR_URL_STATE}/${req.body.email}`);

    // Handle Response
    if (!fetchRes.ok) {
      throw "Failed to find user";
    }

    const fetchResUser = await fetchRes.json();

    // Check password
    const bcryptResult = await bcrypt.compare(req.body.password, fetchResUser.password);
    
    if (!bcryptResult) {
      throw "Incorrect password";
    }

    return res.status(200).send(fetchResUser);
  } catch (e) {
    return res.status(500).send({ message: e });
  }
});

./result.png

Xavier Geerinck © 2020

Twitter - LinkedIn