Friday 18 September 2020

Office Check-In App using Cloud Apllication Programming Model and React.js

 SAP has always helped its customer to run their best. And in this pandemic situation it is no different.

Bearing that in mind, we, from the Brazilian Presales Platform & Technology team, proudly developed an application that controls office access as an effort for a safe return to normality. It is a very simple application, which its source code will be distributed freely to customers. The goal of this app is to ensure that the office always respects its available capacity and employees are able to perform social distancing, in a very simplified manner and at an extremely low cost of implementation. The application flow is demonstrated on the picture below:

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

Application Areas


The Application was divided in three areas: Check-in, Report, and Administration. The check-in area is for general use, while the report and administration areas are available for the system administrator.

Administration

This is where the application would be configured.

◉ Office´s administration
     ◉ In the office’s administration, it is possible to visualize, edit, create and activate access for check-in in the office.

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

◉ Location´s administration
     ◉ In the location’s administration, it is possible to visualize, edit, create and activate access for check-in in the location. Furthermore, it is possible to attribute access controllers for each location.

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

◉ Administrator´s administration
     ◉ In this area is possible to visualize, edit, and create access controllers, which will receive daily a list with users that made check-in in the locations that they administrate.

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

Check-in

This is the area where the user will visualize the available locations and available capacity in selected dates and locations, as well as realize the check-in.

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

Report

Finally, we have the report area, where the administrator will be able to visualize the occupation rates by date and location.

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

Architecture

To build this application, we used the SAP HANA Cloud as database, react.js as the front end framework, the SAP Cloud Application Programming Model with node.js as the back end framework, and Azure Active Directory as an identity provider just as an example to demonstrate the possibilities when working with the SAP Cloud Application Programming Model and the SAP Cloud Platform, Cloud Foundry environment.

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

Prerequisites


One of the beauties from SAP Cloud Platform, Cloud Foundry environment is the ability of using the environment of your choice. In my case, I used the Visual Studio Code to develop locally on my machine, Github as code repository, the SAP Business Application Studio to create the services on the SAP Cloud Platform using a CLI provided by the SAP Business Application Studio, and an SAP Cloud Platform, Cloud Foundry environment instance. Advanced Javascript knowledges and basic knowledges about SAP Cloud Application Programming Model are desirable.

Visual Studio Code

On the Visual Studio Code, we will use Node.js and NPM to install and manage our dependencies and CLIs. 

Github

The front end repository and the back end repository were hosted on Github. Please clone the repository to the desired environment.

SAP Business Application Studio

The Business Application Studio is the next generation of the SAP Web IDE. It comes out of the box with a predefined set of development environments (Dev-Spaces, virtual machines installed on the cloud) and provides the developer more control over their environment and code. I used it to create the services at the SAP Cloud Platform, as the Dev-Spaces are already configured to use the Cloud Foundry CLI, so we can get our application up and running quickly without further headaches about the installation.

SAP Cloud Platform

We will be deploying an SAP HANA Deployment Infrastructure container service in SAP HANA Cloud. We will deploy as well an SRV application to host our API and a Cloud Foundry Application to host our front end.

SAP Cloud Application Programming Model Back End


We used the SAP Cloud Application Programming Model framework on the back end. In this blog post we will be covering only the framework´s aspects regarding the developed application

Core Data Services

Core Data Services is used on this project to declare service definitions and data models. The service definitions will be found in the SRV folder and the data model on the DB folder.

Data Model

We created 7 entities for this application

◉ Users
     ◉ Used to identify the users that makes the check-ins.
◉ SysAdmins
     ◉ Used to declare the users that will configure the system.
◉ CheckIn
     ◉ Used to store the check-ins. It is associated to the user that made the check-in, to the location and office where the check-in was made.
◉ Offices
     ◉  Used to store the offices that will be available for check-in.
◉ Floors
     ◉ Stores the locations and the location capacity that will be available for check-in. It will be associated to offices.
◉ SecurityGuards
     ◉ Stores the access controllers for each location. It is possible to configure a job to send e-mails for these users on a daily basis with a list of users that made check-in for locations that they administrate.
◉ FloorSecurityGuards
     ◉ Many to many association between the floors entity and the security guards entity.

Furthermore, we created 7 views.

◉ Administrators
     ◉ Retrieves data associated to the FloorSecurityGuards entity.
◉ FloorsList
     ◉ Retrieves data associated to the Floors entity.
◉ FloorSecurityGuardsView
     ◉ Retries data related to FloorSecurityGuards entity to send the e-mails.
◉ CheckInList
     ◉ Retrieves data associated to the CheckIn entity.
◉ AvailableCapacity
     ◉ This view will retrieve the available capacity for the location. It uses a subtraction function (just as it would be used in “normal SQL”).
◉ OccupiedCapacity
     ◉ This view will retrieve the occupied capacity by location for reporting purposes. It uses a count function (just as it would be used in “normal SQL”).
◉ DailyCheckInList
     ◉ Retrieves data associated to the CheckIn entity to send e-mails for the access controllers.

Services

Once we have created our data model, we can define services that will retrieve the data from our data model and expose it in an OData endpoint. SAP Cloud Application Programming Model will bootstrap our services into node.js express and create some routes for our services, so we don´t have to worry about writing any javascript code to create an OData API (although we will be writing some javascript code to add some middleware to our endpoints further on). The services are defined on Core Data Services files on the SRV folder. We created two services for this application, one that will be accessed by system administrators and one for both the general user and the system administrator:

◉ AdminService

     ◉ Provides the services for the reporting area and the administrative area.

◉ CatalogService

     ◉ Provides the services for the check-in area.

Once the services are created, you should be able to see the endpoints created for them on the root URL for our API, such as seen below:

SAP HANA Certification, SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Prep

Middlewares

With our services up and running, we can get our hands dirty with some javascript code (don´t worry, it won´t be a lot, I promise). In this case, we have 3 objectives with the javascript code:

◉ Field validation on check-in insert, to prevent the user to insert duplicated check-ins.
◉ Authentication and permissions.
◉ Custom endpoint to send e-mails.

Event Handlers

It is possible to add custom logic for the created services using event handlers. For example, in this application, we added an event handler so that before CAP inserts a check-in, it blocks duplicated entries. As you can see on the code below:

module.exports = (srv) => {

    const { CheckIn, Users } = cds.entities('my.checkinapi')

    srv.before('CREATE', 'CheckIn', async (req) => {
        const checkIn = req.data
        const user = await cds.run(SELECT.from(Users).where({ ID: { '=': checkIn.user.ID } }))

        if (user.length == 0) {
            await cds.run(INSERT.into(Users).entries(checkIn.user))
        }
        const existsCheckIn = await cds.run(SELECT.from(CheckIn).where({ floor_ID: { '=': checkIn.floor_ID }, date: { '=': checkIn.date }, user_ID : {'=' : checkIn.user.ID} }))

        if (existsCheckIn.length > 0) {
            req.reject(400, 'Já existe um check-in para a localidade e data.')
        } else {

            req.data.user_ID = checkIn.user.ID
            delete req.data.user
        }
    })
}

Support for local ./server.js

In this application we used the Azure Active Directory to authenticate users as a demonstration of the SAP Cloud Application Programming Model flexibility. Before we continue, please note that it would be possible to use the user account and authentication service (UAA) to authenticate users through JSON Web Tokens, strategy which was used as well in our application. Moving on to our authentication middleware, to implement this strategy, we will need to add a validation for every request for our API. Although we would be able to add an event handler for each endpoint to validate the access token, that would not be practical. For that reason, it is possible to add a local server.js and listen to the app events. In this scenario, we added our middleware on the bootstrap event, as seen in the code below:

cds.on('bootstrap', (app) => {
    
    // add your own middleware before any by cds are added
    var cors = require('cors')
    app.use(cors())
    app.use((req, res, next) => {
        res.setHeader('Access-Control-Allow-Origin', '*');
        next();
    });

    app.use('/catalog', Auth)
    app.use('/admin', AdminAuth.authenticate)

    app.get('/api/login', AdminAuth.login)  
})

In this code added to the server.js file on the root folder of our API, we will benefit from the bootstrapped express in our app to add cors and access control allow origin header. We will be adding also a token validation for our catalog and admin routes (endpoints for our services) and a custom login route, which will identify the user logged in and return an access token for the remaining routes.

◉ Login method (adminAuth.js)
     ◉ Used to validate if the user is logged in the Azure Active Directory and check if it is a system administrator or not. It will return an access token to validate the access on the remaining endpoints. It uses the jsonwebtoken and the azure-ad-jwt dependencies, which can be installed using NPM. Code below:

exports.login = (req, res, next) => {

    if (req.method === 'OPTIONS') {
        next()
    } else {
        const token = req.headers.idtoken
        if (!token) {
            return res.status(403).send({ errors: ['No token provided.'] })
        }
        aad.verify(token, env.variables.AD_SECRET, async (err, decoded) => {
            if (err) {
                return res.status(403).send({
                    errors: ['Failed to authenticate token.', err],
                    sysAdmin: false
                })
            } else {
                const { SysAdmins } = cds.entities('my.checkinapi')
                
                const sysAdmin = await cds.run(SELECT.from(SysAdmins).where({ email: { '=': decoded.preferred_username } }))
                console.log(sysAdmin)
                if (sysAdmin.length > 0) {
                    const token = jwt.sign({ sysAdmin: sysAdmin[0].email }, env.variables.AD_SECRET)
                    res.json({ sysAdmin: sysAdmin[0].email, token })
                } else {
                    res.json({ sysAdmin: false })
                }
            }
        })
    }
}

◉ Authenticate method (adminAuth.js)
     ◉ This method will validate the access token and check if the user is a system administrator. If the decoded token indicates that the user is not an administrator, access will be denied. It uses the jsonwebtoken dependency. Code below:
exports.authenticate = (req, res, next) => {
    if (req.method === 'OPTIONS') {
        next()
    } else {
        const token = req.headers.idtoken
        if (!token) {
            return res.status(403).send({ errors: ['No token provided.'] })
        }

        jwt.verify(token, env.variables.AD_SECRET, (err, decoded) => {
            if (err) {
                return res.status(403).send({
                    errors: ['Failed to authenticate token.', err]
                })
            } else {
                if (decoded.sysAdmin) {
                    next()
                } else {
                    return res.status(403).send({
                        errors: ['Invalid Token.']
                    })
                }
        
            }
        })
    }
}

◉ Auth method (auth.js)
     ◉ This method will authenticate users for our non-administrative area of the API (catalog service). It will simply validate the access token. It uses the azure-ad-jwt dependency. Code below:

module.exports = (req, res, next) => {
    if(req.method === 'OPTIONS'){
        next()
    }else{

        const token = req.headers.idtoken
        
        if (!token){
            return res.status(403).send({errors: ['No token provided.']})
        }

        aad.verify(token, env.variables.AD_SECRET,(err, decoded) => {
            if (err){
                return res.status(403).send({
                    errors: ['Failed to authenticate token.', err]
                })
            }else{
                req.decoded = decoded
                next()
            }
        })
    }
}

A very important reminder: the token secret should never be stored in code repositories. In node.js it is possible to set your environment variables or simply a file not tracked by git to set theses variables (which it was used in this scenario to simplify the deploy to the SAP Cloud Platform). This file must be required by the files that are going to validate the token. Code below:

exports.variables = {

    AD_SECRET: '<YOUR_SECRET>'


◉ Custom endpoint to send e-mails (server.js)
     ◉ We also created a custom endpoint to send daily e-mails with the employees that made check-in for the respective day, grouped by location that will be sent to the access controllers of each location. Code below:

app.get('/api/checkInList', async (req, res) => {
const { FloorSecurityGuardsView, DailyCheckInList } = cds.entities('my.checkinapi')

const dailyCheckIns = await cds.run(SELECT.from(DailyCheckInList).where({ date: { '=': moment().format('YYYY-MM-DD') } }))
let dailyCheckInsFloors = {}
dailyCheckIns.map((checkIn) => {
if (dailyCheckInsFloors[checkIn.floorID] === undefined) {
dailyCheckInsFloors[checkIn.floorID] = [checkIn]
} else {
dailyCheckInsFloors[checkIn.floorID].push(checkIn)
}
})
Object.keys(dailyCheckInsFloors).map(async (id) => {
const securityGuards = await cds.run(SELECT.from(FloorSecurityGuardsView).where({ floor_ID: { '=': id } }))

if (securityGuards.length > 0) {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: env.variables.MAIL_PROVIDER,
pass: env.variables.MAIL_AUTH
}
})

const emails = securityGuards.map((securityGuard) => {
return securityGuard.securityGuardEmail
})
const office = securityGuards[0].office
const floor = securityGuards[0].floor

transporter.sendMail({
from: env.variables.MAIL_PROVIDER,
to: [emails],
subject: `Lista de check-ins para o dia ${ moment().format('DD/MM/YYYY')}. Localidade: ${office} - ${floor}`,
html: `<h4>${moment().format('DD/MM/YYYY')} ${office} - ${floor}` +
'<table style="border: 1px solid black;border-collapse: collapse;"> <thead> <tr> <th style="border: 1px solid black;border-collapse: collapse;">Nome</th> <th style="border: 1px solid black;border-collapse: collapse;">E-mail</th></tr></thead><tbody>' +
dailyCheckInsFloors[id].map((checkIn) => `<tr><td style="border: 1px solid black;border-collapse: collapse;">${checkIn.userName}</td><td style="border: 1px solid black;border-collapse: collapse;">${checkIn.userEmail}</td></tr>`).join('')
+ "</tbody>" 
}, function (error, info) {
if (error) {
console.log(error)
} else {
console.log('Email sent: ' + info.response)
}
})
}
})

res.status(200).send('success')
})

And that´s enough javascript code for now (we still have to have to add the front-end, but react.js is not the focus of this post). One last reminder: The auth.js, the adminAuth.js and the variables.js file must be in the root folder, otherwise their references won´t be found after deployed to the SAP Cloud Platform, Cloud Foundry environment and this will crash the application (we don´t want this happen, do we?).

React.js framework front end


React.js is the most popular front end framework available right now. It is great to control the application´s states and create componentized assets for the application. The react front-end will use the endpoints created to access the application data and for styling we used the UI5 Web Components for React library, to know more about it, please access the official documentation

Application front end development

The application front end was developed using react.js as mentioned. This is not the focus of this blog post (please feel free to contact me if you want more information about the react.js development). However, I will demonstrate how the authentication was done in the front end, as it is an essential part for the token validation on the back end to function properly and also about the awesome library, UI5 Web Components for React.

MSAL.js and Azure Active Directory

First, we need to register our application in the Azure Active Directory Portal. It is possible to create a free account that will serve our purpose just fine. After creating the free account, please follow this detailed step-by-step that shows how to configure the app in Azure AD. On the last step from that tutorial it will be provided a client secret, please copy it, since it will never be shown again, and this is the secret key that we used on the back end to validate the login. Now that we configured the app in Azure AD, we are going to write some more javascript code, to use MSAL.js on the front end. MSAL.js (Microsoft Authentication Library) is a javascsript library that enables developers to acquire tokens from the Microsoft identity platform endpoint in order to access secured web APIs. To use it in our application, we will NPM import msal into our app and the code needed to add it to our app can be found on the msal folder of the front end repository. In the MsalConfig.jsx file, you will need to add your own clientId from the Azure AD app.

UI5 Web Components for React

The UI5 Web Components for React is a wrapper of the UI5 Web Components. This library was developed with the goal of facilitating the consumption of UI5 controls. It is aimed at developers who want to have more flexibility to use just HTML tags or arbitrary JS frameworks (exactly our case).

SAP Cloud Platform Deploy


After we finish the development, we want to deploy our application to SAP Cloud platform. In my case, I pulled the code from the Github repository into the SAP Business Application Studio, because, as I mentioned before, this environment already has all the CLI and plugins to facilitate the deploy to SAP Cloud Platform. Finally, before we begin our deploy, we need to login into the Cloud Foundry environment from SAP Cloud Platform that we want to use. Please use the command below to login:

cf login

Back end Deployment

Database Deployment

We will deploy our database to SAP HANA Cloud. However, SAP HANA Cloud currently does not support Core Data Services. Fortunately, SAP Cloud application programming Model has the option to transpile the Core Data Services files into hdbtable files (for example). For this to happen, before we run the deploy command, we need to specify in our package.json file that we want to deploy in the hdbtable format, as shown below:

"cds": {

    "hana" : { "deploy-format": "hdbtable" }

}

cds deploy --to hana

This will deploy just the database parts of the project to an SAP HANA Cloud instance. If an hdi-shared service for the project was not created, it will create the service and an HDI container.

Service Deployment

Now that we deployed our database, we can deploy our service. First, we will add an MTA.yaml file in our project with the following command:

cds add mta

Then, we will build the MTA file and generate a .mtar file with the command below:

mbt build -t ./

And finally, we will deploy the .mtar file with the command shown below:

cf deploy <.mtar file>

After command has been executed check on your cloud foundry environment if the application was created and is up and running.

Front end deployment

The front end deployment is much simpler. We simply issue the command below in the CLI, and an application will be created with the name chosen in the command and the start command.

cf p <your_app_name> -c “npm start”

No comments:

Post a Comment