Hi again,

This is the last part in this short series of articles in which we’re building an omni-channel micro app using ONEm Framework and the free api from themoviedb.org.

If you missed the previous articles, you’ll find part 1 here and part 2 here.

All the code is on Github, check the branches labelled “part1”, “part2” and “part3”.

In this last article of the series, I will show you how to recognise different users hitting your micro-app. This will allow you to provide a more personalised user experience and tailor your micro-app’s features and functions according to the particular user. To do this, we’ll make use of the native authentication capabilities of the ONEm Framework.

In part 2, we added a search form that allowed the user to search for a particular movie by title.

Part 2 —Web widget search form and results

Great, but now we want to allow users to maintain their own lists of their favourite movies, so they can come back to them later.

This will require two things:

  1. A database to store users and their favourite movies
  2. A way to recognise returning users so we can show their favourites when they hit the landing menu

Data Model

Our database schema will have two tables (userfavourites and movies). I’ve used dbdiagram.io to document the schema and produce the entity model (it’s also here):

Movies schema
You maybe wondering why the movies table is needed at all, since we could just retrieve the data directly from themoviedb.org. The reason is just to avoid hits on themoviedb API, you’ll see later on.

We’ll use MongoDb and the mongoose module to handle the db operations. Feel free to use any database of your choice:

const Mongoose = require('mongoose')
const UserFavouriteSchema = new Mongoose.Schema({
    user_id: { type: String, required: true },
    _movie: {
        type: Mongoose.Schema.Types.ObjectId,
        ref: 'movies',
        required: true
 }})
const UserFavourite = Mongoose.model('userfavourites', UserFavouriteSchema)
const MovieSchema = new Mongoose.Schema({
    movie_id: { type: String, required: true },
    title: { type: String, required: true }
})
const Movie = Mongoose.model('movies', MovieSchema)
module.exports = {
    UserFavourite,
    Movie
}

Save the above in app_api/models/index.js

Identifying Users

The database implementation is pretty simple and will be familiar to many of you. But how do we know which user is accessing our micro-app? When the ONEm Framework issues callbacks to the micro-app’s callback_url, a JSON Web Token (JWT) is included in the HTTP header of each request. If you’re not familiar with JWTs, there’s an excellent description here.

The JWT issued by the ONEm Framework can be easily decoded, there are many open source modules out there to choose from. Once decoded, the sub property (or claim in JWT terminology), will contain our unique user id.

The JWT issued by ONEm Framework contains lots of goodies relating to the user accessing your micro-app and also some hints about the capabilities of the channel the user is using. A full description can be found in the ONEm Framework documentation.

Verifying the JWT

In order to verify that the token we get from the ONEm Framework is bona-fide, we should need to verify the signature. The JWT will be signed using a the token secret value we used when we created the micro app. This was not mentioned in part 1, but eager-eyed reader will have spotted this field:

Token secret field in the developer portal

The token will appear in all callbacks issued by ONEm Framework in the Authorization header like this:

Authorization: Bearer <token>

We’ll use express middleware and jwt-simple to verify the token. The following code cause the token to be extracted from the header and verified on all /api routes:

const jwt = require('jwt-simple')
...
/*
 * Middleware to verify the token and save the user in req.user
 */
function getUser(req, res, next) {
    if (!req.header('Authorization')) {
        return res.status(401).send({ message: 'Unauthorized request' })
    }
    const token = req.header('Authorization').split(' ')[1]
    const payload = jwt.decode(token, process.env.TOKEN_SECRET)
    if (!payload) {
        return res.status(401).send({ message: 'Unauthorized Request'
    }
    req.user = payload.sub
    next()
}
api.use(getUser)

Ensure that the TOKEN_SECRET environment variable is configured in your .env file and that its value matches that of the developer portal:

PORT=3000READ_ACCESS_TOKEN=
<movie api token>
TOKEN_SECRET=87654321

Add to favourites

When a user has viewed a movie, we want to allow them to add the movie to their favourites list or go back and search again:

Add to favourites button

If the movie isn’t already a favourite of the current user, we’ll present an option to “Add to favourites”. So let’s update views/movieView.pug:

section
    header #{movie.title}
    img(src=movie.poster_path alt="poster")
    p #{movie.overview}
    p Released: #{new Date(movie.release_date).toLocaleString()}
    ul
        if !isFavourite
            li
                a(href="/user/favourite/"+movie.id+"?title="+movie.title method="post") Add to favourites
        li
            a(href="/") Search again

When the user selects “Add to favourites”, the ONEm Framework will issue a HTTP POST (note the method=”post”) to /api/user/favourite/{movieId}

...
const { UserFavourite, Movie } = require('../models')
...
api.post('/user/favourite/:movieId', async (req, res) => {
    try {
        const movieId = req.params.movieId
        const title = req.query.title
        const user = req.user
        const movie = await Movie.findOneAndUpdate(
            { movie_id: movieId },
            { movie_id: movieId, title: title },
            { upsert: true, new: true }
        )
        await UserFavourite.findOneAndUpdate(
            { user_id: user, _movie: movie._id },
            { user_id: user, _movie: movie._id },
            { upsert: true, new: true }
        )
        const userFavourites = await UserFavourite.find({ user_id: user }).populate('_movie')
        const rootTag = loadTemplate(views.VIEW_LANDING, { userFavourites: userFavourites })
        const response = Response.fromTag(rootTag)
        res.json(response.toJSON())
        } catch (e) {
            res.status(500).json({ success: false, message: 'server error' })
        }
})

We grab the user id, including from the HTTP request in this line:

const user = req.user

After the favourite has been added, we display the landing view. We want that view to show the updated favourite under “My favourites”, like this:

New landing view

Let’s update app_api/views/landing.pug to reflect the new requirement:

section(auto-select)
    header Menu
    img(src="https://www.themoviedb.org/assets/2/v4/logos/408x161-powered-by-rectangle-green-bb4301c10ddc749b4e79463811a68afebeae66ef43d17bcfd8ff0e60ded7ce99.png" alt="powered by themoviedb.org")
    ul
        li
            a(href="/search") Search for a movie
        li My favourites (#{userFavourites.length})
        each favourite in userFavourites
            li
                a(href='/movie/'+favourite._movie.movie_id) #{favourite._movie.title}

You may have noticed in the first line we used the “auto-select” attribute. This tells ONEm Framework that if the resulting menu comprises only one item, that item is automatically selected. So if the user doesn’t have any favourites, the only option will be “Search for a movie” which will be auto-selected. No point in asking the user to choose from a menu comprising only one option.

Auto-select and other html attributes are documented here.

I have omitted some code for the sake of brevity, you can find the complete code in the git repository referenced at the top of this article. To quick start, you can use the following sequence:

$ git clone https://github.com/chrishornmem/movie-microapp.git && cd movie-microapp
$ git checkout part3
$ npm install
# Ensure the .env is created as per the instructions, then
$ node index

Once you have the new version up and running, you can also try out the SMS interface using the Test Client tab in the developer portal.

Thanks for reading, if you’re interested in learning more about ONEm Framework, you can checkout our technical documentation here.