Express

2.1 Overview

Express.js is the web development framework which is used for backend server.

2.2 Loaders/Settings

Before the server can listen to port, we need to load require modules and set configurations.

2.2.1 Import NPM Packages

In the loader, firstly we need to import required npm packages, which are Express, Path, cookie, morgan, and cors.

File Location loaders\express.js

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cookieSession = require('cookie-session');
const cors = require('cors')

Use Packages

const app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cors({
credentials: true,
origin: config.allowedOrigins
}));
app.use(cookieParser());
app.use(cookieSession(config.cookieSession));
app.use(express.static(path.join(__dirname, 'public')));

2.2.2 Set and Import Server Configuration

Setting which URL and port are used for each component of the backend architecture. Also, setting the expired time and password hashing method.

The following setting is default: 1) The port used for GraphDB is 7200 2) The port used for MongoDB is 27017 3) The port used for Mail Server is 25. The host mailing server is currently mail.lesterlyu.com. Also, we currently use no-reply@lesterlyu.com to send automatic emails. 4) The cookie keep maximum 24 hours in the browser. 5) The time to keep temporarily file is maximum 1 hour. 6) The time to keep the validation token available is maximum 1 hour.

File Location config\index.js

graphdb: {
addr: `http://${isDocker ? 'host.docker.internal' : 'localhost'}:7200`,
},
mongodb: {
addr: `mongodb://${isDocker ? 'host.docker.internal' : 'localhost'}:27017/${process.env.test ? "cmmpTest" : "cmmp"}`
},
allowedOrigins: ['http://localhost:3000', 'http://localhost:3002',
'http://64.52.29.182:3000', 'http://64.52.29.182:3002'],
frontend: {
addr: 'http://64.52.29.182:3002' || 'http://localhost:3000'
},
// pbkdf2 configuration, ~70ms with this config
passwordHashing: {
digest: 'sha512',
iterations: 100000, // 100,000 is sufficient
hashSize: 64, // in bytes
saltSize: 32 // in bytes
},
cookieSession: {
keys: ['secret', 'keys'],
maxAge: 24 * 60 * 60 * 1000 // expires in 24 hours
},
mailServer: {
host: 'mail.lesterlyu.com',
port: 25,
auth: {
user: 'no-reply',
pass: 'passw0rd.'
},
forceTLS: true,
},
mailConfig: {
from: 'no-reply@lesterlyu.com',
},
jwtConfig: {
secret: 'secret keys',
options: {expiresIn: 60 * 60 * 24} // 24 hour
},
autoRemoveUnused: { // in minutes
upload: 60,
user: 60 * 24,
}

File Location loaders\express.js

const config = require('../config');

2.2.3 Use Packages

After importing package and setting configurations, let the express use the package by configuration

const app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cors({
credentials: true,
origin: config.allowedOrigins
}));
app.use(cookieParser());
app.use(cookieSession(config.cookieSession));
app.use(express.static(path.join(__dirname, 'public')));

2.2.4 Import Routers and Middlewares

Import all of the Routers and Middlewares

const indexRouter = require('../routes/index');
const usersRouter = require('../routes/users');
const OpportunityListingsRouter = require('../routes/opportunityListings');
const uploadRouter = require('../routes/upload');
const interestRouter = require('../routes/interest')
const authMiddleware = require('../services/middleware/auth');
const errorHandler = require('../services/middleware/errorHandler');

Let Routers and Middlewares connect to the express server

app.use('/', indexRouter);
app.use('/', authMiddleware());
app.use('/users', usersRouter);
app.use('/opportunities', OpportunityListingsRouter);
app.use('/upload', uploadRouter);
app.use('/interests', interestRouter);
app.use(errorHandler);

2.2.5 Other

Setting time zone to Toronto Time

process.env.TZ = 'America/Toronto'

2.2.6 Load App

Load the express in app.js.

const app = require('./loaders/express');
process.on('SIGINT', function () {
console.log('Received SIGINT.');
process.exit(0);
});
process.on('SIGTERM', function () {
console.log('Received SIGTERM.');
process.exit(0);
});
module.exports = app;

2.3 Routes

Routes connect the URL to the backend services or models directly, which provides the Restful API supports the frontend User Interface

2.3.1 Add a new router

To create a new Route, create a new javascript file in /route folder (e.g. route/exampleRoute.js), and setting up as following

File route/exampleRoute.js

// Head of the file
const express = require('express');
const router = express.Router();
// Bottom of the file
module.exports = router;

In file express.js, need to add

const exampleRouter = require('../routes/exampleRoute');
app.use('/exampleroute', exampleRouter);

2.3.2 Add a new API

In the middle of the router file, you can setting up the URL path and implement the functionality directly, the following is added into the middle of the file route/exampleRoute.js

router.get('/test', async (req, res, next) => {
return res.send("example")
});

The route above implement an api, when hitting the URL /exampleroute/test, you will get a message example

Moreover, you can setup the parameters to req.param by the following

router.get('/test/:id', async (req, res, next) => {
return res.send(`example ${req.param.id}`)
});

When hitting the URL /exampleroute/test/1, you will get a message example 1

Also, you can setup req.body by setting json of the form from the frontend, for example

router.get('/form', async (req, res, next) => {
const submittedForm = req.body
// Just given an funcition given a function name
ModelFunction.dealwithForm(submittedForm)
return res.send(`success`)
});

2.4 Services

You can put functions which are used for routers into different files in the service folder, for example, create a file exampleService.js.

const testService = async (req, res, next) => {
return res.send("example")
}
module.exports = {testService}

Also, in the file services/index, put the test service into the variable which needs to be exported

module.exports = {
loginService: require('./login')
}

Then in the file route/exampleRoute.js, you can write the following instead, please don't forget to import the function from services.

const {testService} = require('../services');
router.get('/test', testService);

2.4.1 Middleware

The middleware runs before or after when executing function from services which are for routes. Current middlewares are in the following

2.4.1.1 Force authorization

When using the API, check whether the user is logged in or not

File location: services\middleware\auth.js

const forceAuthentication = (message) => (req, res, next) => {
if (req.session.username) {
next();
}
else {
res.json({error: true, code: 2, message: message || 'Authentication required.'})
}
};

2.4.1.2. Error Handle

Return error message when causing an error in service function

File location: services\middleware\errorHandler.js

function errorHandler (err, req, res, next) {
console.error(err);
const {message, statusCode, stack, ...others} = err;
res.status(statusCode || 500).json({
success: false,
detail: others,
message: message,
stack: isProduction ? undefined : stack,
});
}

2.5 Models

In our application, models are classes which involves GraphDB database. It follows the object oriented programming. Class files may include GraphDB model and/or MongoDB model with operations and/or helper functions.

All of the javascript files for models are in /model subdirectory

The following is an example /model/example

const GraphDBExampleModel = ...
const MongoDBExampleModel = ...
class Example {
constructor(example) {
//...
}
save(){
//...
}
}

2.6 Utils

Utils includes extra tools are used in the application

2.6.1 Not Implemented Errors

The following utils\error\index.js handles the error to remind the developers that the class method is not implemented.

class NotImplementedError extends Error {
constructor(name = 'Unset') {
super(`${name} is not implemented.`);
}
}

2.6.2 Hashing

The utils\hashing has the method to encrypt and validate the password.

hashPassword: Encrypt the password, returns encrypted password and salt.

validatePassword: Validate the password.

checkPasswordFormat: Check the password is alphanumeric, at least 8 charactors, at least 1 uppercase and 1 lowercase letter.

2.6.3 Mailer

We use nodemailer to send automatic emails, the code is in the folder utils/mailer

To create a new mailer, first, create a file exampleMailer.js import nodemailer and mailer config

const nodemailer = require('nodemailer');
const {mailConfig, mailServer} = require('../../config');

Then, you can import html file for the template of the email page, for example in template.js

const {frontend} = require('../../config');
const getVerificationTemplate = () => `<!DOCTYPE html>
<html>
<head>
<title> Example Title</title>
</head>
<body>
<p> Test </p>
</body>
</html>

Then you can add the following in the exampleMailer.js`

const {getTemplate} = require("./template");

Then you can implement sending email function by the following

const sendEmail = async (email) => {
const mailOptions = {
...mailConfig,
to: email,
subject: 'Subjest title',
html: getVerificationTemplate()
};
await new Promise((resole, reject) => {
transporter.sendMail(mailOptions, function (err) {
if (err) {
reject(err);
} else {
console.log("email sent");
resole();
}
});
});
};

2.6.4 GraphDB

The implementation of helper functions in GraphDB are in utils\graphdb, the description is in GraphDB section.

2.6.5 Sleep

The sleep function implemented in utils\index.js , is to give the time limit to load repository of GraphDB and file attachment of MongoDB