/**
* @module leaderboard-api
* @desc The leaderboard-api main module.
* @version 1.0.0
* @author Essam A. El-Sherif
*/
/* Import node.js core modules */
import crypto from 'node:crypto';
import fs from 'node:fs';
import http from 'node:http';
import https from 'node:https';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
/* Import package dependencies */
import express from 'express';
import morgan from 'morgan';
import dotenv from 'dotenv';
import redis from 'redis';
/**
* @func expressjwt
* @param {object} options
* @desc Third-party middleware function.
*/
import { expressjwt } from 'express-jwt';
/**
* @const {object} authRouter - Authentication router object.
* @see module:auth-route.router
*/
import { router as authRouter } from './route-auth/index.js';
/**
* @const {object} scoreRouter - Score router object.
* @see module:score-route.router
*/
import { router as scoreRouter } from './route-score/index.js';
/**
* @const {object} leaderboardRouter - Leaderboard router object.
* @see module:leaderboard-route.router
*/
import { router as leaderboardRouter } from './route-leaderboard/index.js';
/* Emulate commonJS __filename and __dirname constants */
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/* Configure dotenv path to read the package .env file */
dotenv.config({path: join(__dirname, './.env')});
/** @const {object} appOptions - The application options. */
const appOptions = {
serverProt: process.env.lb_serverProtocol,
serverCert: process.env.lb_serverCert,
serverKey : process.env.lb_serverKey,
serverHost: process.env.lb_serverHost,
serverPort: process.env.lb_serverPort,
serverPath: process.env.lb_serverPath,
secretKey : crypto.randomBytes(32).toString('hex'),
connectRd : process.env.lb_connectRedis,
serverName: `${process.env.lb_serverName} (${process.platform}) NodeJS/${process.version.substring(1)}`,
verbose: true,
};
/**
* @const {object} app - The express app object.
* @desc The express app object conventionally denotes the express application.
*/
const app = express();
/* Application built-in settings */
app.set('x-powered-by', false);
app.set('etag', false);
/* Application User-defined settings */
app.set('serverName', appOptions.serverName);
app.set('secretKey', appOptions.secretKey);
app.set('redisKeyUser', 'lb:user:');
app.set('redisKeyActivity', 'lb:act:');
app.set('redisKeyTimestamp','lb:ts:');
/*
* Use morgan() third-party middleware function
* for logging HTTP requests, only if verbose is true.
*/
appOptions.verbose && app.use(morgan('common', { immediate: true }));
/*
* Use express.json() built-in middleware function
* for parsing incoming requests with JSON payloads.
*/
app.use(express.json());
/* Endpoint: .../ Method: GET */
app.get(`${appOptions.serverPath}`, (req, res) => {
let resBody = '';
resBody += `${appOptions.serverHost}:${appOptions.serverPort}\n`;
resBody += `${req.app.get('serverName')}\n`;
res.setHeader('Server', req.app.get('serverName'));
res.status(200).send(resBody);
});
/* Endpoint: .../auth */
app.use(`${appOptions.serverPath}/auth`, authRouter);
/* Endpoint: .../score */
app.use(`${appOptions.serverPath}/score`, expressjwt({ secret: app.get('secretKey'), algorithms: ['HS256'] }), scoreRouter);
/* Endpoint: .../leaderboard */
app.use(`${appOptions.serverPath}/leaderboard`, expressjwt({ secret: app.get('secretKey'), algorithms: ['HS256'] }), leaderboardRouter);
/* Error Handling */
app.use((err, req, res, next) => {
if(err.name === 'UnauthorizedError'){
res.status(401).send(`Authorization Error: ${err.message}`);
}
else{
next();
}
});
/*
* https://github.com/redis/node-redis
*
* Connect to Redis and attach a node-redis Client instance to app.
* Create a node-redis Client instance, or throws on failure.
*/
try{
app.set('clientRd',
redis.createClient({
url: appOptions.connectRd,
name: 'leaderboard-api'
})
.on('error', (err) => {
console.error(err.message);
process.exit(1);
})
);
/*
* Since v4 of node-redis, the client does not automatically connect to the server.
* Instead you need to run .connect() after creating the client or you will receive an error.
*/
await app.get('clientRd').connect();
appOptions.verbose && console.log(`... Redis database server is connected`);
}
catch(err){
console.error(err.message);
process.exit(1);
}
/* Start the application server */
let serverOptions = {};
let serverModule = http;
if(appOptions.serverProt === 'https'){
try{
serverOptions.key = fs.readFileSync(join(__dirname, appOptions.serverKey));
serverOptions.cert = fs.readFileSync(join(__dirname, appOptions.serverCert));
serverModule = https;
}
catch(err){
appOptions.serverProt = 'http';
serverModule = http;
}
}
/** @const {object} server - The http/https server object. */
const server = serverModule.createServer(serverOptions, app)
.listen(appOptions.serverPort, appOptions.serverHost, () => {
const {port, address:host} = server.address();
appOptions.verbose && console.log(
`... leaderboard-api server is listening to ${appOptions.serverProt}://${host}:${port}`
);
appOptions.verbose && console.log(
'... Press CTRL-C to terminate'
);
appOptions.verbose && console.log();
})
.on('error', (err) => {
appOptions.verbose && console.error(err.code, err.message);
});