1
0
Fork 0
master
no_name_user_7718 2023-12-09 14:50:50 +03:00
commit daa09e5628
40 changed files with 1466 additions and 0 deletions

23
.env.example Normal file
View File

@ -0,0 +1,23 @@
# SERVICE
PORT=8000
NODE_ENV=production
SERVICE_NAME=nwa
# DATABASE
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=secret
DB_NAME=postgres
DB_DIALECT=postgres
# USERS
USER_JWT_ACCESS_SECRET=secret
USER_JWT_REFRESH_SECRET=secret
USER_JWT_ACCESS_EXPIRE=3600
USER_JWT_REFRESH_EXPIRE=36000
# ADMINS
ADMIN_JWT_ACCESS_SECRET=secret
ADMIN_JWT_REFRESH_SECRET=secret
ADMIN_JWT_ACCESS_EXPIRE=259200
ADMIN_JWT_REFRESH_EXPIRE=259200

0
.eslintignore Normal file
View File

30
.eslintrc.js Normal file
View File

@ -0,0 +1,30 @@
module.exports = {
'env':
{
'browser': true,
'commonjs': true,
'es2021': true
},
'extends': 'eslint:recommended',
'overrides': [],
'parserOptions': {
'ecmaVersion': 'latest'
},
'rules': {
'no-unused-vars': 'off',
'no-undef': 'off',
'no-var': 0,
'indent': [
'error',
2
],
'linebreak-style': [
'error',
'windows'
],
'semi': [
'error',
'always'
]
}
};

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
.env
package-lock.json
*.log

6
.scripts/pre-commit Normal file
View File

@ -0,0 +1,6 @@
#!/bin/sh
echo '{
"buildChecksum": "'$(git rev-parse HEAD)'",
"buildTime": "'$(date +%s000)'"
}' > build.json
git add build.json

8
.sequelizerc Normal file
View File

@ -0,0 +1,8 @@
const path = require('path');
module.exports = {
'config': path.resolve('server/configs', 'db.js'),
'seeders-path': path.resolve('server/dao', 'seeders'),
'migrations-path': path.resolve('server/dao', 'migrations'),
'models-path': path.resolve('server/dao', 'models')
};

63
README.md Normal file
View File

@ -0,0 +1,63 @@
# Obmen77-backend (Node.js)
## List of contents
- [Technologies used](#technologies-and-software-used)
- [List of libraries used](#list-of-libraries-used)
- [How to use](#how-to-use)
## Technologies used
| Technology name | Link |
|-----------------|------|
| Node.js |[Website](https://nodejs.org/)|
| PostgreSQL |[Website](https://www.postgresql.org/)|
## List of libraries used
| Library name | Purpose | Link |
|--------------|---------|------|
| bluebird |for parallel asynchronous operations ```await Promise.all([])```|[Website](https://www.npmjs.com/package/bluebird)|
| camelcase |for parsing filenames|[Website](https://www.npmjs.com/package/camelcase)|
| decamelize |for parsing filenames|[Website](https://www.npmjs.com/package/decamelize)|
| dotenv |for environment variables|[Website](https://www.npmjs.com/package/dotenv)|
| express |framework of the entire application for the API|[Website](https://www.npmjs.com/package/express)|
| cors |for corses|[Website](https://www.npmjs.com/package/cors)|
| morgan |for logging requests|[Website](https://www.npmjs.com/package/morgan)|
| log4js |for logging requests|[Website](https://www.npmjs.com/package/log4js)|
| chalk |for logging requests|[Website](https://www.npmjs.com/package/chalk)|
| pg |to work with the database |[Website](https://www.npmjs.com/package/pg)|
| pg-hstore |to work with the database |[Website](https://www.npmjs.com/package/pg-hstore)|
| sequelize |as an ORM for working with the database|[Website](https://www.npmjs.com/package/sequelize)|
| sequelize-auto-migrations |for automatic creation of migrations from ORM models|[Website](https://www.npmjs.com/package/sequelize-auto-migrations)|
| sequelize-cli |for database operations|[Website](https://www.npmjs.com/package/sequelize-cli)|
| ws |to work with the web socket|[Website](https://www.npmjs.com/package/ws)|
| joi |for validation|[Website](https://www.npmjs.com/package/joi)|
# How to use
## List of contents
- [Pre commit hook](#pre-commit-hook)
- [Run application](#run-application)
## Pre commit hook
Copy the .scripts/pre-commit file to the `.git/hooks/` folder to automatically fill in the build.json file before the commit.
## Run application
1. Copy .env.example, change its name to .env, and populate the file according to your environment;
2. Run the command:
```bash
npm i
```
to set dependencies;
3. Run the command:
```bash
npm run db:migrate:create
```
4. Run the command:
```bash
npm run db:seed
```
5. Execute the command:
```bash
npm run start
```
to run the application.

25
app.js Normal file
View File

@ -0,0 +1,25 @@
require('dotenv').config();
require('./server/utils/check-dotenv');
const logger = require('./server/utils/override-console-log');
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const chalk = require('chalk');
const {routify} = require('./server/api');
const app = express();
app.use(cors());
app.use(morgan(chalk`:method :url :status :res[content-length] - :response-time ms`, {
stream: {
write: function (message) {
logger.info(message.trim());
}
}
}));
app.use(express.json({limit: '1024mb'}));
routify(app);
module.exports = app;

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "nwa",
"version": "1.0.0",
"description": "Node.js backend template",
"main": "index.js",
"scripts": {
"start": "node ./server",
"db:makemigrations": "node ./node_modules/sequelize-auto-migrations/bin/makemigration.js --name auto-migration",
"db:migrate": "sequelize-cli db:migrate",
"db:migrate:create": "sequelize-cli db:create && npm run db:migrate",
"db:seed": "sequelize-cli db:seed:all",
"db:drop:BIG_CONFIRM_I_AM_SURE_TO_DELETE_DATABASE": "sequelize-cli db:drop",
"lint": "eslint ./server",
"lint:fix": "eslint ./server --fix"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "4.1.2",
"cors": "2.8.5",
"dotenv": "16.3.1",
"express": "4.18.2",
"joi": "17.11.0",
"jsonwebtoken": "9.0.2",
"log4js": "6.9.1",
"morgan": "1.10.0",
"pg": "8.7.3",
"pg-hstore": "2.3.4",
"sequelize": "6.23.2",
"sequelize-auto-migrations": "github:Scimonster/sequelize-auto-migrations",
"sequelize-cli": "6.5.1"
},
"devDependencies": {
"eslint": "8.55.0"
}
}

93
server/api/Controller.js Normal file
View File

@ -0,0 +1,93 @@
const ApplicationError = require('../utils/application-error');
const validMethod = {
get: 'get',
post: 'post',
delete: 'delete',
patch: 'patch',
put: 'put'
};
/**
* A class that represents a controller for processing HTTP routes.
*/
class Controller {
/**
* Creates a new instance of the Controller class.
*
* @param {Object} options Options for the controller.
* @param {string} options.method HTTP method (for example, 'get', 'post', 'delete', 'patch', 'put').
* @param {string} options.path The route path (URL).
* @param {function} options.handler The request handler that will be called when the route is accessed.
* @param {Joi.Schema} options.validationSchema Validation scheme for validating input data.
* @param {function[]} options.middlewares Middleware to be called before the handler.
*/
constructor({ method, path, handler, validationSchema, middlewares }) {
if (!validMethod[method] || !path || !handler) {
console.error(`Controller misconfiguration. Method: ${method}, path: ${path}, handler exists: ${!!handler}`);
process.exit(-1);
}
this.method = method;
this.path = path;
this.handler = handler;
this.validationSchema = validationSchema;
this.middlewares = middlewares;
}
/**
* Validates input data according to the validation scheme.
*
* @param {Express.Request} req The Express request object.
* @param {Express.Response} res The Express response object.
* @throws {ApplicationError.JsonValidation} If the data does not match the validation scheme.
*/
async validate(req, res) {
if (!this.validationSchema) {
return;
}
const objectToValidate = this.method === 'get' ? req.query : req.body;
try {
const value = await this.validationSchema.validateAsync(objectToValidate, {stripUnknown: true});
if (this.method === 'get') {
req.query = value;
} else {
req.body = value;
}
} catch (e) {
const message = e.details.map(i => i.message).join(',');
throw ApplicationError.JsonValidation(message);
}
}
/**
* Performs HTTP request processing.
*
* @param {Express.Request} req The Express request object.
* @param {Express.Response} res The Express response object.
* @param {function} next Function for further processing of the request.
*/
async run(req, res, next) {
try {
const handlerResult = await this.handler(req, res, next);
if (!res.headersSent) {
return res.status(200).json({ success: true, statusCode: 200, data: handlerResult });
}
} catch (e) {
next(e);
}
}
/**
* Registers the controller with the Express router.
* @param {Express.Router} router The Express router in which the controller will be registered.
*/
register(router) {
const self = this;
const handle = async function (req, res, next) {
return await self.run(req, res, next);
};
this.middlewares.forEach(middleware => {
router[this.method](this.path, middleware) ;
});
router[this.method](this.path, handle);
}
}
module.exports = Controller;

View File

@ -0,0 +1,24 @@
const Joi = require('joi');
/**
* @module An object to retrieve information about the administrator.
* @type {Object}
* @property {string} method - HTTP method (get).
* @property {string} path - The path to process the request.
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middlewares that can be applied to the request processing (empty array).
* @property {Function} handler - The function that handles the request.
* @async
* @param {Object} req - The request object.
* @param {Object} res - Response object.
* @returns {Object} - An object that contains information.
* @throws {Error} - An exception if an error occurred while processing the request.
*/
module.exports = {
method: 'get',
path: '/whoami',
validationSchema: Joi.object({}),
middlewares: [],
handler: async function (req, res) {
return req.admin;
}
};

16
server/api/admin/index.js Normal file
View File

@ -0,0 +1,16 @@
const decodeJwt = require('../../middlewares/verify-admin-jwt');
const unpackAccessToken = require('../../middlewares/unpack-access-token');
const registerControllers = require('../../utils/register-controllers');
const catchWrap = require('../../utils/exception-catch-wrapper');
/**
* @module Module for configuring routes and registering public scopes controllers.
* @param {Object} router - The Express router object.
* @param {string} prefix - Prefix for route paths.
*/
module.exports = (router, prefix) => {
router.use(prefix, catchWrap(unpackAccessToken));
router.use(prefix, catchWrap(decodeJwt));
registerControllers(__dirname, prefix, router);
};

19
server/api/index.js Normal file
View File

@ -0,0 +1,19 @@
const exceptionHandler = require('../middlewares/exception-handler');
const prefix = '/api/v1/';
/**
* Prefix for all API version 1 routes.
* @type {string}
*/
exports.prefix = prefix;
/**
* A function for configuring the Express application routing.
*
* @param {Express.Application} app The Express object of the application.
*/
exports.routify = (app) => {
require('./public')(app, prefix + 'public');
require('./user')(app, prefix + 'user');
require('./admin')(app, prefix + 'admin');
app.use(exceptionHandler);
};

View File

@ -0,0 +1,33 @@
const {Admin} = require('../../../../dao/models');
const Joi = require('joi');
const ApplicationError = require('../../../../utils/application-error');
/**
* @module An object to autorizate the administrator.
* @type {Object}
* @property {string} method - HTTP method (post).
* @property {string} path - The path to process the request.
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middlewares that can be applied to the request processing (empty array).
* @property {Function} handler - The function that handles the request.
* @async
* @param {Object} req - The request object.
* @param {Object} res - Response object.
* @returns {Object} - An object that contains information.
* @throws {Error} - An exception if an error occurred while processing the request.
*/
module.exports = {
method: 'post',
path: '/login/admin',
validationSchema: Joi.object({
email: Joi.string().required(),
password: Joi.string().required()
}),
middlewares: [],
handler: async function (req, res) {
const admin = await Admin.scope('auth').findOne({where: {email: req.body.email}});
if (!admin) {
throw ApplicationError.NotFound();
}
return await admin.authenticateByPassword(req.body.password);
}
};

View File

@ -0,0 +1,34 @@
const {User} = require('../../../../dao/models');
const Joi = require('joi');
const ApplicationError = require('../../../../utils/application-error');
/**
* @module An object representing the Post HTTP method for user authorization.
* @type {Object}
* @property {string} method - HTTP method (post).
* @property {string} path - Path to process the request ("/login").
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middleware that can be used to process the request (empty array).
* @property {Function} handler - The function that handles the request for user authorization.
* @async
* @param {Object} req - Request object.
* @param {Object} res - Response object.
* @returns {Object} - An object containing user data after authorization.
* @throws {Error} - Exception if an error occurred during authorization.
*/
module.exports = {
method: 'post',
path: '/login',
validationSchema: Joi.object({
email: Joi.string().required(),
password: Joi.string().required(),
}),
middlewares: [],
handler: async function (req, res) {
const user = await User.scope('auth').findOne({where: {email: req.body.email}});
if (!user) {
throw ApplicationError.NotFound();
}
return await user.authenticateByPassword(req.body.password);
}
};

View File

@ -0,0 +1,32 @@
const {Admin} = require('../../../../dao/models');
const Joi = require('joi');
const ApplicationError = require('../../../../utils/application-error');
/**
* @module An object to authenticate the administrator.
*
* @property {string} method - HTTP method (post).
* @property {string} path - The path to process the request.
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middlewares that can be applied to the request processing (empty array).
* @property {Function} handler - The function that handles the request.
* @async
* @param {Object} req - The request object.
* @param {Object} res - Response object.
* @returns {Object} - An object that contains information.
* @throws {Error} - An exception if an error occurred while processing the request.
*/
module.exports = {
method: 'post',
path: '/refresh/admin',
validationSchema: Joi.object({
refreshToken: Joi.string().required(),
}),
middlewares: [],
handler: async function (req, res) {
const admin = await Admin.scope('auth').findOne({where: {refreshToken: req.body.refreshToken}});
if (!admin) {
throw ApplicationError.NotFound();
}
return await admin.authenticateByRefresh();
}
};

View File

@ -0,0 +1,32 @@
const {User} = require('../../../../dao/models');
const Joi = require('joi');
const ApplicationError = require('../../../../utils/application-error');
/**
* @module An object representing the HTTP Post method for updating the user's access token.
* @type {Object}
* @property {string} method - HTTP method (post).
* @property {string} path - Path to process the request ("/refresh").
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middleware that can be applied to the request processing (empty array).
* @property {Function} handler - The function that handles the request to update the user's access token.
* @async
* @param {Object} req - The request object.
* @param {Object} res - Response object.
* @returns {Object} - An object containing the new access token after the update.
* @throws {Error} - Exception if an error occurred while updating the access token.
*/
module.exports = {
method: 'post',
path: '/refresh',
validationSchema: Joi.object({
refreshToken: Joi.string().required(),
}),
middlewares: [],
handler: async function (req, res) {
const user = await User.scope('auth').findOne({where: {refreshToken: req.body.refreshToken}});
if (!user) {
throw ApplicationError.NotFound();
}
return await user.authenticateByRefresh();
}
};

View File

@ -0,0 +1,37 @@
const {User} = require('../../../../dao/models');
const Joi = require('joi');
const ApplicationError = require('../../../../utils/application-error');
/**
* @module An object representing the Post HTTP method for creating a user.
* @type {Object}
* @property {string} method - HTTP method (post).
* @property {string} path - The path to process the request ("/register").
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middlewares that can be applied to the request processing (empty array).
* @property {Function} handler - The request handler function for creating a user.
* @async
* @param {Object} req - Request object.
* @param {Object} res - Response object.
* @throws {Error} - Exception if an error occurred during creation.
*/
module.exports = {
method: 'post',
path: '/register',
validationSchema: Joi.object({
email: Joi.string().required(),
password: Joi.string().required(),
}),
middlewares: [],
handler: async function (req, res) {
const oldUser = await User.findOne({where: {email: req.body.email}});
if (oldUser) {
throw ApplicationError.NotUnique();
}
req.body.passwordHashed = req.body.password;
delete req.body.password;
const user = await User.create(req.body);
return await user.getTokens();
}
};

View File

@ -0,0 +1,33 @@
const Joi = require(`joi`);
const fs = require('fs');
const util = require("util");
const readFile = util.promisify(fs.readFile);
/**
* @module An object representing the HTTP GET method for obtaining information about the application version.
* @type {Object}
* @property {string} method - HTTP method (get).
* @property {string} path - The path to process the request ("/version").
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middlewares that can be applied to the request processing (empty array).
* @property {Function} handler - The function that handles the request to get information about the application version.
* @async
* @param {Object} req - The request object.
* @param {Object} res - Response object.
* @returns {Object} - An object that contains information about the version of the application.
* @throws {Error} - An exception if an error occurred while processing the request.
*/
module.exports = {
method: `get`,
path: `/version`,
validationSchema: Joi.object({}),
middlewares: [],
handler: async function (req, res) {
const buildFile = await readFile('build.json');
const build = JSON.parse(buildFile);
return res.status(200).json({
"buildChecksum": build.buildChecksum,
"buildTime": build.buildTime,
});
}
};

View File

@ -0,0 +1,10 @@
const registerControllers = require('../../utils/register-controllers');
/**
* @module Module for configuring routes and registering public scopes controllers.
* @param {Object} router - The Express router object.
* @param {string} prefix - Prefix for route paths.
*/
module.exports = (router, prefix) => {
registerControllers(__dirname, prefix, router);
};

View File

@ -0,0 +1,24 @@
const Joi = require('joi');
/**
* @module An object to retrieve information about the user.
* @type {Object}
* @property {string} method - HTTP method (get).
* @property {string} path - The path to process the request.
* @property {Joi.ObjectSchema} validationSchema - Input validation scheme.
* @property {Array} middlewares - Middlewares that can be applied to the request processing (empty array).
* @property {Function} handler - The function that handles the request.
* @async
* @param {Object} req - The request object.
* @param {Object} res - Response object.
* @returns {Object} - An object that contains information.
* @throws {Error} - An exception if an error occurred while processing the request.
*/
module.exports = {
method: 'get',
path: '/whoami',
validationSchema: Joi.object({}),
middlewares: [],
handler: async function (req, res) {
return req.user;
}
};

16
server/api/user/index.js Normal file
View File

@ -0,0 +1,16 @@
const decodeJwt = require('../../middlewares/verify-user-jwt');
const unpackAccessToken = require('../../middlewares/unpack-access-token');
const registerControllers = require('../../utils/register-controllers');
const catchWrap = require('../../utils/exception-catch-wrapper');
/**
* @module Module for configuring routes and registering public scopes controllers.
* @param {Object} router - The Express router object.
* @param {string} prefix - Prefix for route paths.
*/
module.exports = (router, prefix) => {
router.use(prefix, catchWrap(unpackAccessToken));
router.use(prefix, catchWrap(decodeJwt));
registerControllers(__dirname, prefix, router);
};

29
server/configs/db.js Normal file
View File

@ -0,0 +1,29 @@
require('dotenv').config();
const fs = require('fs');
/**
* @module Database configuration object
*/
module.exports = {
'development': {
'host': process.env.DB_HOST,
'username': process.env.DB_USER,
'password': process.env.DB_PASSWORD,
'database': process.env.DB_NAME,
'dialect': process.env.DB_DIALECT,
'logging': false
},
'production': {
'host': process.env.DB_HOST,
'port': process.env.DB_PORT,
'username': process.env.DB_USER,
'password': process.env.DB_PASSWORD,
'database': process.env.DB_NAME,
'dialect': process.env.DB_DIALECT,
'logging': false,
// 'dialectOptions': {
// ssl: {
// ca: fs.readFileSync(__dirname + '/db.crt')
// }
// }
}
};

View File

@ -0,0 +1,181 @@
'use strict';
var Sequelize = require('sequelize');
/**
* Actions summary:
*
* createTable "Admin", deps: []
* createTable "User", deps: []
*
**/
var info = {
"revision": 1,
"name": "auto-migration",
"created": "2023-12-05T16:14:52.852Z",
"comment": ""
};
var migrationCommands = function(transaction) {
return [{
fn: "createTable",
params: [
"Admin",
{
"id": {
"type": Sequelize.INTEGER,
"field": "id",
"autoIncrement": true,
"primaryKey": true,
"allowNull": false
},
"email": {
"type": Sequelize.STRING,
"field": "email",
"allowNull": false,
"unique": true
},
"passwordHashed": {
"type": Sequelize.STRING,
"field": "passwordHashed",
"allowNull": false,
"unique": false
},
"salt": {
"type": Sequelize.STRING,
"field": "salt",
"allowNull": false,
"unique": false
},
"refreshToken": {
"type": Sequelize.STRING,
"field": "refreshToken",
"allowNull": true,
"unique": false
},
"createdAt": {
"type": Sequelize.DATE,
"field": "createdAt",
"allowNull": false
},
"updatedAt": {
"type": Sequelize.DATE,
"field": "updatedAt",
"allowNull": false
}
},
{
"transaction": transaction
}
]
},
{
fn: "createTable",
params: [
"User",
{
"id": {
"type": Sequelize.INTEGER,
"field": "id",
"autoIncrement": true,
"primaryKey": true,
"allowNull": false
},
"email": {
"type": Sequelize.STRING,
"field": "email",
"allowNull": false,
"unique": true
},
"passwordHashed": {
"type": Sequelize.STRING,
"field": "passwordHashed",
"allowNull": false,
"unique": false
},
"salt": {
"type": Sequelize.STRING,
"field": "salt",
"allowNull": false,
"unique": false
},
"refreshToken": {
"type": Sequelize.STRING,
"field": "refreshToken",
"allowNull": true,
"unique": false
},
"createdAt": {
"type": Sequelize.DATE,
"field": "createdAt",
"allowNull": false
},
"updatedAt": {
"type": Sequelize.DATE,
"field": "updatedAt",
"allowNull": false
}
},
{
"transaction": transaction
}
]
}
];
};
var rollbackCommands = function(transaction) {
return [{
fn: "dropTable",
params: ["Admin", {
transaction: transaction
}]
},
{
fn: "dropTable",
params: ["User", {
transaction: transaction
}]
}
];
};
module.exports = {
pos: 0,
useTransaction: true,
execute: function(queryInterface, Sequelize, _commands)
{
var index = this.pos;
function run(transaction) {
const commands = _commands(transaction);
return new Promise(function(resolve, reject) {
function next() {
if (index < commands.length)
{
let command = commands[index];
console.log("[#"+index+"] execute: " + command.fn);
index++;
queryInterface[command.fn].apply(queryInterface, command.params).then(next, reject);
}
else
resolve();
}
next();
});
}
if (this.useTransaction) {
return queryInterface.sequelize.transaction(run);
} else {
return run(null);
}
},
up: function(queryInterface, Sequelize)
{
return this.execute(queryInterface, Sequelize, migrationCommands);
},
down: function(queryInterface, Sequelize)
{
return this.execute(queryInterface, Sequelize, rollbackCommands);
},
info: info
};

View File

@ -0,0 +1,99 @@
{
"tables": {
"Admin": {
"tableName": "Admin",
"schema": {
"id": {
"allowNull": false,
"primaryKey": true,
"autoIncrement": true,
"field": "id",
"seqType": "Sequelize.INTEGER"
},
"email": {
"unique": true,
"allowNull": false,
"field": "email",
"seqType": "Sequelize.STRING"
},
"passwordHashed": {
"unique": false,
"allowNull": false,
"field": "passwordHashed",
"seqType": "Sequelize.STRING"
},
"salt": {
"unique": false,
"allowNull": false,
"field": "salt",
"seqType": "Sequelize.STRING"
},
"refreshToken": {
"unique": false,
"allowNull": true,
"field": "refreshToken",
"seqType": "Sequelize.STRING"
},
"createdAt": {
"allowNull": false,
"field": "createdAt",
"seqType": "Sequelize.DATE"
},
"updatedAt": {
"allowNull": false,
"field": "updatedAt",
"seqType": "Sequelize.DATE"
}
},
"indexes": []
},
"User": {
"tableName": "User",
"schema": {
"id": {
"allowNull": false,
"primaryKey": true,
"autoIncrement": true,
"field": "id",
"seqType": "Sequelize.INTEGER"
},
"email": {
"unique": true,
"allowNull": false,
"field": "email",
"seqType": "Sequelize.STRING"
},
"passwordHashed": {
"unique": false,
"allowNull": false,
"field": "passwordHashed",
"seqType": "Sequelize.STRING"
},
"salt": {
"unique": false,
"allowNull": false,
"field": "salt",
"seqType": "Sequelize.STRING"
},
"refreshToken": {
"unique": false,
"allowNull": true,
"field": "refreshToken",
"seqType": "Sequelize.STRING"
},
"createdAt": {
"allowNull": false,
"field": "createdAt",
"seqType": "Sequelize.DATE"
},
"updatedAt": {
"allowNull": false,
"field": "updatedAt",
"seqType": "Sequelize.DATE"
}
},
"indexes": []
}
},
"revision": 1
}

View File

@ -0,0 +1,89 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const ApplicationError = require('../../utils/application-error');
module.exports = (sequelize, Sequelize) => {
const Admin = sequelize.define('Admin', {
email: {
type: Sequelize.STRING,
validate: {
isEmail: true
},
unique: true,
allowNull: false,
set(val) {
this.setDataValue('email', val.toString().toLowerCase());
}
},
passwordHashed: {
type: Sequelize.STRING,
unique: false,
allowNull: false,
set(val) {
this.setDataValue('salt', crypto.randomBytes(16).toString('hex'));
this.setDataValue('passwordHashed', this.hashPassword(val));
}
},
salt: {
type: Sequelize.STRING,
unique: false,
allowNull: false,
},
refreshToken: {
type: Sequelize.STRING,
unique: false,
allowNull: true,
}
}, {
freezeTableName: true,
defaultScope: {
attributes: { exclude: ['salt', 'passwordHashed', 'refreshToken'] }
},
scopes: { auth: {} }
});
Admin.prototype.hashPassword = function (password) {
if (!this.salt || !password) {
throw ApplicationError.RequiredAttributes();
}
let hash = crypto.createHmac('sha512', this.salt);
hash.update(password);
return hash.digest('hex');
};
Admin.prototype.authenticateByPassword = async function (password) {
if (!password || this.passwordHashed !== this.hashPassword(password)) {
throw ApplicationError.InvalidCredentials();
}
return this.getTokens();
};
Admin.prototype.authenticateByRefresh = async function () {
try {
jwt.verify(this.refreshToken, process.env.ADMIN_JWT_REFRESH_SECRET);
} catch(err) {
throw ApplicationError.BadToken();
}
return this.getTokens();
};
Admin.prototype.getTokens = async function () {
const ttlAccess = Number.parseInt(process.env.ADMIN_JWT_ACCESS_EXPIRE);
const ttlRefresh = Number.parseInt(process.env.ADMIN_JWT_REFRESH_EXPIRE);
const accessExpireDate = Date.now() + (ttlAccess * 1000);
const payload = {id: this.id};
this.refreshToken = jwt.sign(payload, process.env.ADMIN_JWT_REFRESH_SECRET, {expiresIn: ttlRefresh});
await this.save();
return {
expire: accessExpireDate,
token: jwt.sign(payload, process.env.ADMIN_JWT_ACCESS_SECRET, {expiresIn: ttlAccess}),
refresh: this.refreshToken,
};
};
return Admin;
};

View File

@ -0,0 +1,27 @@
const Sequelize = require('sequelize');
const fs = require('fs');
const path = require('path');
const dbConfig = require('../../configs/db.js');
const sequelize = new Sequelize(dbConfig[process.env.NODE_ENV]);
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
const basename = path.basename(__filename);
fs.readdirSync(__dirname).filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
}).forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
module.exports = db;

88
server/dao/models/user.js Normal file
View File

@ -0,0 +1,88 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const ApplicationError = require('../../utils/application-error');
module.exports = (sequelize, Sequelize) => {
const User = sequelize.define('User', {
email: {
type: Sequelize.STRING,
validate: {
isEmail: true
},
unique: true,
allowNull: false,
set(val) {
this.setDataValue('email', val.toString().toLowerCase());
}
},
passwordHashed: {
type: Sequelize.STRING,
unique: false,
allowNull: false,
set(val) {
this.setDataValue('salt', crypto.randomBytes(16).toString('hex'));
this.setDataValue('passwordHashed', this.hashPassword(val));
}
},
salt: {
type: Sequelize.STRING,
unique: false,
allowNull: false,
},
refreshToken: {
type: Sequelize.STRING,
unique: false,
allowNull: true,
}
}, {
freezeTableName: true,
defaultScope: {
attributes: { exclude: ['salt', 'passwordHashed', 'refreshToken'] }
},
scopes: {auth: {}}
});
User.prototype.hashPassword = function (password) {
if (!this.salt || !password) {
throw ApplicationError.RequiredAttributes();
}
let hash = crypto.createHmac('sha512', this.salt);
hash.update(password);
return hash.digest('hex');
};
User.prototype.authenticateByPassword = async function (password) {
if (!password || this.passwordHashed !== this.hashPassword(password)) {
throw ApplicationError.InvalidCredentials();
}
return this.getTokens();
};
User.prototype.authenticateByRefresh = async function () {
try {
jwt.verify(this.refreshToken, process.env.USER_JWT_REFRESH_SECRET);
} catch(err) {
throw ApplicationError.BadToken();
}
return this.getTokens();
};
User.prototype.getTokens = async function () {
const ttlAccess = Number.parseInt(process.env.USER_JWT_ACCESS_EXPIRE);
const ttlRefresh = Number.parseInt(process.env.USER_JWT_REFRESH_EXPIRE);
const accessExpireDate = Date.now() + (ttlAccess * 1000);
const payload = {id: this.id};
this.refreshToken = jwt.sign(payload, process.env.USER_JWT_REFRESH_SECRET, {expiresIn: ttlRefresh});
await this.save();
return {
expire: accessExpireDate,
token: jwt.sign(payload, process.env.USER_JWT_ACCESS_SECRET, {expiresIn: ttlAccess}),
refresh: this.refreshToken,
};
};
return User;
};

View File

@ -0,0 +1,20 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
queryInterface.sequelize.query('ALTER SEQUENCE "Admin_id_seq" RESTART WITH 2');
return queryInterface.bulkInsert('Admin', [
{
id: 1,
email: 'admin@admin.com',
passwordHashed: 'e4874dbe86b2075b5eb9c9da2abadd29c1d0f709c21762edbcdb08d0a281bc1790838290e15ce6688c8e52777c7667b9a5816551d6d1e403658b95d604906b7e',
salt: '9bdfa3726c9a889bfb3eed605902cd32',
createdAt: new Date(),
updatedAt: new Date()
}
], {});
},
down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('Admin', null, {});
}
};

14
server/index.js Normal file
View File

@ -0,0 +1,14 @@
const app = require('./../app');
const cluster = require('cluster');
/**
* Server startup
*/
if (cluster.isMaster) {
require('./worker')();
} else {
const port = process.env.PORT || 8080;
const server = app.listen(port, () => {
console.log(`App running on port ${port}...`);
});
server.setTimeout(3600000);
}

View File

@ -0,0 +1,22 @@
const ApplicationError = require('../utils/application-error');
/**
* Middleware to handle errors at the application level.
*
* This middleware handles different types of errors.
* It generates an appropriate response based on the type of error and the error message.
*
* @param {Error} err - The error to be handled.
* @param {Object} req - The Express request object.
* @param {Object} res - The Express response object.
* @param {function} next - The function to move to the next middleware.
*/
module.exports = async (err, req, res, next) => {
if (err instanceof ApplicationError) {
console.error(err);
return res.status(err.httpStatusCode).json({ error: err.message, code: err.code, isSuccess: false, body: err.body });
}
console.error(err);
return res.status(500).json({ error: err.message, code: -1, isSuccess: false });
};

View File

@ -0,0 +1,22 @@
const ApplicationError = require('../utils/application-error');
/**
* Middleware for checking for and decrypting authorization tokens.
*
* This middleware checks for and decrypts the authorization token passed in the request header.
* If the token is missing or has an invalid format, an error is generated.
*
* @param {Object} req - Express request object.
* @param {Object} res - The Express response object.
* @param {function} next - A function to move to the next middleware.
* @throws {ApplicationError} Error if the token is missing or has an invalid format.
*/
module.exports = async (req, res, next) => {
const header = req.headers['authorization'];
if (!header || !header.includes(' ')) {
throw ApplicationError.NoAuthHeader();
}
req.accessToken = header.split(' ')[1];
next();
};

View File

@ -0,0 +1,29 @@
const { Admin } = require('../dao/models');
const ApplicationError = require('../utils/application-error');
const jwt = require('jsonwebtoken');
/**
* Middleware for verifying and processing the administrator's token.
*
* This middleware checks for the presence and validity of the administrator token in the request.
* If the check is successful, the administrator object is added to the `req.admin` property
* property for further use in other parts of the application. If the token is missing or invalid,
* an error of the type `ApplicationError.BadToken` or `ApplicationError.AdminTokenNotFound` is thrown.
*
* @param {object} req - Express request object.
* @param {object} res - The Express response object.
* @param {function} next - The function to go to the next middleware.
*/
module.exports = async (req, res, next) => {
let adminJwt;
try {
adminJwt = jwt.verify(req.accessToken, process.env.ADMIN_JWT_ACCESS_SECRET);
} catch(err) {
throw ApplicationError.BadToken();
}
req.admin = await Admin.findOne({ where: { id: adminJwt.id }, attributes: { exclude: ['salt', 'passwordHashed'] }});
if (!req.admin) {
throw ApplicationError.AdminTokenNotFound();
}
next();
};

View File

@ -0,0 +1,29 @@
const { User } = require('../dao/models');
const ApplicationError = require('../utils/application-error');
const jwt = require('jsonwebtoken');
/**
* Middleware for verifying and processing user tokens.
*
* This middleware checks for the presence and validity of the user token in the request.
* If the user token is valid, the user object is added to the `req.user` property
* property for further use in other parts of the application. If the token is missing or invalid,
* an error of type `ApplicationError.BadToken` is thrown.
*
* @param {object} req - Express request object.
* @param {object} res - The Express response object.
* @param {function} next - A function to move to the next middleware.
*/
module.exports = async (req, res, next) => {
let userJwt;
try {
userJwt = jwt.verify(req.accessToken, process.env.USER_JWT_ACCESS_SECRET);
} catch(err) {
throw ApplicationError.BadToken();
}
req.user = await User.findOne({ where: { id: userJwt.id }, attributes: { exclude: ['salt', 'passwordHashed']}});
if (!req.user) {
throw ApplicationError.BadToken();
}
next();
};

View File

@ -0,0 +1,32 @@
/**
* The `ApplicationError` class represents a special error that can occur in the program.
* This class allows you to create errors with different codes, HTTP statuses and messages.
*
* @class
* @extends Error
*/
class ApplicationError extends Error {
static NotUnique = (message) => new ApplicationError(1, 409, message || 'The item is not unique');
static NotFound = () => new ApplicationError(2, 404, 'Item not found');
static NotCreated = () => new ApplicationError(3, 400, 'Element not created');
static RequiredAttributes = () => new ApplicationError(4, 400, 'Required attributes are not specified');
static JsonValidation = (message) => new ApplicationError(5, 422, `Invalid JSON format: ${message}`);
static AlreadyExists = () => new ApplicationError(6, 409, 'Already exists');
static BadTransaction = () => new ApplicationError(7, 400, 'Invalid transaction');
static InvalidCredentials = () => new ApplicationError(8, 401, 'Invalid credentials');
static BadToken = () => new ApplicationError(9, 401, 'Bad token');
static AdminTokenNotFound = () => new ApplicationError(10, 401, 'Admin token not found');
static NoAuthHeader = () => new ApplicationError(11, 403, 'Authorization header not specified');
constructor(code, httpStatusCode, message, body) {
super();
this.code = code;
this.httpStatusCode = httpStatusCode;
this.message = message;
this.body = body;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ApplicationError;

View File

@ -0,0 +1,20 @@
/**
* @module Checks for required environment variables and terminates the process if they are missing.
*/
const dotenvVariables = [
'NODE_ENV',
'PORT',
'SERVICE_NAME'
];
const missingDotenvVariables = [];
dotenvVariables.forEach(variable => {
if (!process.env[variable]) {
missingDotenvVariables.push(`${variable}`);
}
});
if (missingDotenvVariables.length) {
console.error(`Following dotenv variables are missing: ${missingDotenvVariables}`);
process.exit(-1);
}

View File

@ -0,0 +1,10 @@
/**
* @module Wrapper for handling errors in asynchronous middleware functions.
*
* @param {Function} fn - Asynchronous middleware function to process the request and response.
* @returns {Function} - Middleware that passes errors to the Express error handler.
*/
module.exports = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next); };
};

View File

@ -0,0 +1,70 @@
const log4js = require('log4js');
/**
* @module Configuring and creating a logger based on the log4js library.
*
* This module configures and creates a logger based on the log4js library. It also provides.
* The ability to redirect console output through the logger to process logs.
*/
log4js.addLayout('json', function(config) {
return (logEvent) => {
if (logEvent.error) {
logEvent.errorMessage = logEvent.error.message;
logEvent.errorStack = logEvent.error.stack;
logEvent.errorName = logEvent.error.name;
}
if (logEvent.data && logEvent.data[0] && typeof logEvent.data[0] === 'string') {
logEvent.message = logEvent.data[0];
}
if (logEvent.errorMessage) {
logEvent.message = logEvent.errorMessage;
}
let result = JSON.stringify(logEvent);
result += config.separator;
return result;
};
});
log4js.configure({
appenders: {
out: {
type: 'stdout',
layout: {
type: 'pattern',
pattern: '%[[%dZ] [%p]%] - %m',
tokens: {}
}
},
app: {
type: "file",
filename: "application.log",
layout: { type: 'json', separator: ',' }
},
},
categories: {
default: { appenders: ['out', 'app'], level: 'all' },
[`[${process.env.SERVICE_NAME}]`]: { appenders: ['out', 'app'], level: 'all' }
},
disableClustering: true
});
const logger = log4js.getLogger();
logger.level = 'all';
(function () {
console.error = function (errMessage) {
logger.error(errMessage);
};
console.log = function (logMessage) {
if (process.env.NODE_ENV === 'production') return;
logger.debug(logMessage);
};
console.warn = function (warnMessage) {
logger.info(warnMessage);
};
})();
module.exports = logger;

View File

@ -0,0 +1,45 @@
const fs = require('fs');
const path = require('path');
const Controller = require('../api/Controller');
const getDirsAndFileLists = (dirname) => {
const data = {};
const items = fs.readdirSync(dirname, { withFileTypes: true });
const files = [];
items.forEach((item) => {
if (item.isDirectory()) {
const subdata = getDirsAndFileLists(path.join(dirname, item.name));
Object.assign(data, subdata);
} else {
files.push(item.name);
}
});
data[dirname] = files;
return data;
};
/**
* @module Module for dynamic loading and registration of controllers in the router.
*
* This module scans the specified directory and its subdirectories for controllers,
* downloads them, and registers them in the transferred router.
*
* @param {string} dirname - The path to the directory where the controllers are located.
* @param {string} prefix - Prefix for the path of the controllers.
* @param {Object} router - The router object to register the controllers.
*/
module.exports = (dirname, prefix, router) => {
const dirsAndFiles = getDirsAndFileLists(path.join(dirname, "controllers"));
for (let dir in dirsAndFiles) {
dirsAndFiles[dir].forEach((fileName) => {
const controllerParams = require(path.join(dir, fileName));
controllerParams.path = prefix + controllerParams.path;
const controller = new Controller(controllerParams);
controller.register(router);
});
}
};

41
server/worker.js Normal file
View File

@ -0,0 +1,41 @@
const cluster = require('cluster');
let workers = [];
/**
* @module A module for creating and managing multiple workflows (worker) in Node.js.
*/
module.exports = () => {
// to read number of cores on system
let numCores = require('os').cpus().length;
console.log('Master cluster setting up ' + numCores + ' workers');
// iterate on number of cores need to be utilized by an application
// current example will utilize all of them
for(let i = 0; i < numCores; i++) {
// creating workers and pushing reference in an array
// these references can be used to receive messages from workers
workers.push(cluster.fork());
// to receive messages from worker process
workers[i].on('message', function(message) {
console.log(message);
});
}
// process is clustered on a core and process id is assigned
cluster.on('online', function(worker) {
console.log('Worker ' + worker.process.pid + ' is listening');
});
// if any of the worker process dies then start a new one by simply forking another one
cluster.on('exit', function(worker, code, signal) {
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.log('Starting a new worker');
cluster.fork();
workers.push(cluster.fork());
// to receive messages from worker process
workers[workers.length-1].on('message', function(message) {
console.log(message);
});
});
};