Compare commits

..

10 Commits

Author SHA1 Message Date
32735ad7ff boom!
overall: formatting check, jsdoc type hints, express res/return stuff

utils - changes in logger, dateformatter and removed unneeded ones

.env file changes

license check, readme update

package.json update - version, deps, URLs

server cleanup

sequelize config check
2024-08-14 21:08:58 +05:30
7318e8e325
oops 2023-09-22 20:27:33 -07:00
Kaushik Narayan R
47527d443f
Create LICENSE 2023-09-21 16:48:41 -07:00
Kaushik Narayan R
ba4a4e1fcd
Update README.md before going public 2023-09-21 16:43:06 -07:00
62ed623c7e packages fix, validator update 2023-05-14 04:55:25 +05:30
ca0c8e96d4 utils 2023-05-12 23:40:39 +05:30
af932c038d captcha, db check 2023-01-23 22:52:04 +05:30
1f0b049cb7 Folder structure, READMEs 2022-11-12 16:37:02 +05:30
92f121e60c Update package.json 2022-11-12 09:18:49 +00:00
501e43d51d Update README.md 2022-11-12 09:18:22 +00:00
39 changed files with 572 additions and 4710 deletions

3
.env
View File

@ -1,5 +1,8 @@
PORT=5000 PORT=5000
TRUST_PROXY = 1
AUTOMAILER_SMTP_SERVICE
AUTOMAILER_SMTP_HOST
AUTOMAILER_ID = "mailerID@mailserver.domain" AUTOMAILER_ID = "mailerID@mailserver.domain"
AUTOMAILER_APP_PASSWD = "mailerpasswd" AUTOMAILER_APP_PASSWD = "mailerpasswd"

View File

@ -1,5 +1,8 @@
DB_USERNAME='your_local_db_username' DB_USER = your_database_username
DB_PASSWORD='your_local_db_password' DB_PASSWD = your_database_password
DB_NAME='your_local_db_name' DB_NAME = your_database_name
DB_HOST = localhost
DB_PORT = your_database_port
DB_DIALECT = your_database_dialect
CAPTCHA_SECRET = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" CAPTCHA_SECRET = 'your-captcha-secret'

3
.env.production Normal file
View File

@ -0,0 +1,3 @@
DB_URL = 'your_database_connection_string'
CAPTCHA_SECRET = 'your-captcha-secret'

101
.gitignore vendored
View File

@ -6,14 +6,101 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories # Dependency directories
node_modules/ node_modules/
# dotenv environment variables file # TypeScript v1 declaration files
.env.development typings/
.env.staging
.env.production
*.env
# Data files # TypeScript cache
*/*.csv *.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.local
.env.*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# SQLite db
*.db

View File

@ -2,5 +2,5 @@ require("dotenv-flow").config();
const path = require("path"); const path = require("path");
module.exports = { module.exports = {
"config": path.resolve("config", "config.js") "config": path.resolve("config", "sequelize.js")
}; };

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Kaushik Narayan R
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,18 +1,17 @@
# Express-Sequelize backend server template # Express-Sequelize backend server template
### To get started: ## To get started
- Clone this repo: `git clone https://gitlab.com/ctf-tech-2023/backend-template`
- Reset the git remote repo URL: `git remote rm origin`
- Set new git remote URL: `git remote add origin https://gitlab.com/ctf-tech-2023/new-repo-name`
### Project setup: - Clone this repo: `git clone https://github.com/20kaushik02/express-sequelize-backend-template`
- Reset the git remote repo URL: `git remote rm origin`
- Set new git remote URL: `git remote add origin https://github.com/20kaushik02/<<new-repo-name>>`
- Remove the template environment files from git alone: `git rm -r --cached *.env*`
## Project setup
- Edit `package.json` to reflect the new name and URLs - Edit `package.json` to reflect the new name and URLs
- Edit `README.md` to reflect project details
- Run `npm i` to install all dependencies - Run `npm i` to install all dependencies
- Before running `sequelize-cli` commands while developing, make sure to set `$env:NODE_ENV='development'` on Windows, or `NODE_ENV=development` on Linux/MacOS - Before running `sequelize-cli` commands while developing, make sure to set `$env:NODE_ENV='development'` on Windows, or `NODE_ENV=development` on Linux/MacOS
- Env config: - [See here](https://github.com/kerimdzhanov/dotenv-flow?tab=readme-ov-file#files-under-version-control) for best practices for .env files configuration
- **.env** - All things common to all environments (port, mailer creds, JWT secret, admin data access creds, etc.)
- **.env.development** - Development environment (dev captcha secret, dev DB details)
- **.env.staging** - Staging environment (dev captcha secret, staging DB conn. string) - **for sysadmins**
- **.env.production** - Production environment (production captcha secret, prod DB conn. string) - **for sysadmins**
- Staging: `npm run staging_prep` and `npm run staging` to deploy on Render after configuring a new web service on Render dashboard - Staging: `npm run staging_prep` and `npm run staging` to deploy on Render after configuring a new web service on Render dashboard

1
boilerplates/README.md Normal file
View File

@ -0,0 +1 @@
# Boilerplates - reusable code templates

View File

@ -1,6 +1,7 @@
const typedefs = require("../typedefs");
const logger = require("../utils/logger")(module); const logger = require("../utils/logger")(module);
const typedefs = require("../typedefs");
/** /**
* Business logic to go in these controller functions. * Business logic to go in these controller functions.
* Everything should be contained inside try-catch blocks * Everything should be contained inside try-catch blocks
@ -12,8 +13,9 @@ const __controller_func = async (req, res) => {
try { try {
} catch (error) { } catch (error) {
res.sendStatus(500);
logger.error("__controller_func", { error }); logger.error("__controller_func", { error });
return res.status(500).send({ message: "Server Error. Try again." }); return;
} }
} }

View File

@ -1,14 +1,13 @@
const router = require("express").Router(); const router = require("express").Router();
const { validate } = require("../validators"); const { validate } = require("../validators");
const { __controller_func } = require("./controller");
router.get( router.get(
// URL, // URL,
// middleware, // middleware,
// validators, // validators,
// validate, // validate,
// __controller_func // controller
); );
router.post( router.post(

View File

@ -24,5 +24,4 @@ const __validator_func = async (req, res, next) => {
module.exports = { module.exports = {
__validator_func, __validator_func,
} };

1
config/README.md Normal file
View File

@ -0,0 +1 @@
# Configuration files and data

View File

@ -1,23 +0,0 @@
module.exports = {
"development": {
"username": process.env.DB_USERNAME, // local PostgreSQL DB username
"password": process.env.DB_PASSWORD, // local PostgreSQL DB password
"host": "127.0.0.1", // localhost
"database": process.env.DB_NAME, // local PostgreSQL DB name
"dialect": "postgres"
},
"staging": {
"use_env_variable": "DB_URL", // staging database connection string
"dialect": "postgres",
"dialectOptions": {
"ssl": true,
},
},
"production": {
"use_env_variable": "DB_URL", // production database connection string
"dialect": "postgres",
"dialectOptions": {
"ssl": true,
},
}
}

28
config/sequelize.js Normal file
View File

@ -0,0 +1,28 @@
const logger = require("../utils/logger")(module);
const connConfigs = {
development: {
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'postgres',
host: process.env.DB_HOST || '127.0.0.1',
port: process.env.DB_PORT || 5432,
},
staging: {
use_env_variable: "DB_URL", // use connection string for non-dev env
},
production: {
use_env_variable: "DB_URL", // use connection string for non-dev env
// dialectOptions: {
// ssl: true,
// },
}
}
// common config
for (const conf in connConfigs) {
connConfigs[conf]['logging'] = (msg) => logger.debug(msg);
connConfigs[conf]['dialect'] = process.env.DB_DIALECT || 'postgres';
}
module.exports = connConfigs;

1
controllers/README.md Normal file
View File

@ -0,0 +1 @@
# Controllers - business logic functions, end of the API route

View File

@ -1,23 +1,35 @@
require("dotenv-flow").config(); require("dotenv-flow").config();
const util = require('util');
const express = require("express"); const express = require("express");
const cors = require("cors"); const cors = require("cors");
const helmet = require("helmet"); const helmet = require("helmet");
const logger = require("./utils/logger")(module); const logger = require("./utils/logger")(module);
const app = express(); const app = express();
app.use(express.json()); // Enable this if you run behind a proxy (e.g. nginx)
app.use(express.urlencoded({ extended: true })); app.set('trust proxy', process.env.TRUST_PROXY);
app.use(cors()); app.use(cors());
app.use(helmet()); app.use(helmet());
app.disable("x-powered-by"); app.disable("x-powered-by");
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Static
app.use(express.static(__dirname + '/static'));
// Put routes here // Put routes here
app.use((_req, res) => { // Fallbacks
return res.status(200).send("Back-end for"); app.use((req, res) => {
res.status(200).send("Back-end for");
logger.info("Unrecognized URL", { url: req.url });
return;
}); });
const port = process.env.PORT || 5000; const port = process.env.PORT || 5000;
@ -25,3 +37,20 @@ const port = process.env.PORT || 5000;
app.listen(port, () => { app.listen(port, () => {
logger.info(`App Listening on port ${port}`); logger.info(`App Listening on port ${port}`);
}); });
const cleanupFunc = (signal) => {
if (signal)
logger.info(`${signal} signal received, shutting down now...`);
Promise.allSettled([
// handle DB conn, sockets, etc. here
util.promisify(server.close),
]).then(() => {
logger.info("Cleaned up, exiting.");
process.exit(0);
});
}
['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach((signal) => {
process.on(signal, () => cleanupFunc(signal));
});

1
keys/README.md Normal file
View File

@ -0,0 +1 @@
# Keys - public/private key pairs for certificate QR signing and verification

1
middleware/README.md Normal file
View File

@ -0,0 +1 @@
# Middleware - functionalities that must be in the middle of the API route control flow

37
middleware/admin.js Normal file
View File

@ -0,0 +1,37 @@
const logger = require("../utils/logger")(module);
const typedefs = require("../typedefs");
const creds = JSON.parse(process.env.ADMIN_CREDS);
/**
* Middleware to validate admin access
* @param {typedefs.Req} req
* @param {typedefs.Res} res
* @param {typedefs.Next} next
*/
const adminQueryCreds = async (req, res, next) => {
try {
/** @type {any} */
const { user, access } = req.query;
if (creds[user] === access) {
logger.info("Admin access - " + user);
next();
}
else {
// we do a bit of trolling here
const unauthIP = req.headers['x-real-ip'] || req.ip
res.status(401).send("Intruder alert. IP address: " + unauthIP);
logger.warn("Intruder alert.", { ip: unauthIP });
return;
}
} catch (error) {
res.sendStatus(500);
logger.error("adminQueryCreds", { error });
return;
}
}
module.exports = {
adminQueryCreds,
};

39
middleware/captcha.js Normal file
View File

@ -0,0 +1,39 @@
const fetch = require("cross-fetch");
const logger = require("../utils/logger")(module);
const typedefs = require("../typedefs");
/**
* Google ReCAPTCHA v2 verification
*
* @param {typedefs.Req} req
* @param {typedefs.Res} res
* @param {typedefs.Next} next
*/
const verifyCaptcha = async (req, res, next) => {
try {
const secretKey = process.env.CAPTCHA_SECRET;
const verifyCaptchaURL = `https://google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${req.body.captcha}`;
const captchaResp = await fetch(verifyCaptchaURL);
const captchaData = await captchaResp.json();
if (captchaData.success !== undefined && !captchaData.success) {
res.status(403).send({
message: "Failed captcha verification"
});
logger.error("Recaptcha", { captchaData });
return;
}
next();
} catch (error) {
res.sendStatus(500);
logger.error("verifyCaptcha", { error });
return;
}
}
module.exports = {
verifyCaptcha
};

1
migrations/README.md Normal file
View File

@ -0,0 +1 @@
# Sequelize migrations folder

1
models/README.md Normal file
View File

@ -0,0 +1 @@
# Sequelize model schema

View File

@ -2,13 +2,12 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const Sequelize = require("sequelize"); const Sequelize = require("sequelize");
const logger = require("../utils/logger")(module);
const basename = path.basename(__filename); const basename = path.basename(__filename);
const env = process.env.NODE_ENV || "development"; const env = process.env.NODE_ENV || "development";
const config = require(__dirname + "/../config/config.js")[env]; const config = require(__dirname + "/../config/sequelize.js")[env];
const db = {}; const db = {};
// Create new Sequelize instance
let sequelize; let sequelize;
if (config.use_env_variable) { if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config); sequelize = new Sequelize(process.env[config.use_env_variable], config);
@ -16,6 +15,16 @@ if (config.use_env_variable) {
sequelize = new Sequelize(config.database, config.username, config.password, config); sequelize = new Sequelize(config.database, config.username, config.password, config);
} }
(async () => {
try {
await sequelize.authenticate();
logger.info("Sequelize auth success");
} catch (error) {
logger.error("Sequelize auth error", { err });
throw error;
}
})();
// Read model definitions from folder // Read model definitions from folder
fs fs
.readdirSync(__dirname) .readdirSync(__dirname)

4514
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "backend-template", "name": "backend-template",
"version": "1.3.0", "version": "2.0.0",
"description": "Template for back-end server using Express and Sequelize.", "description": "Template for a back-end server using Express and Sequelize.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development nodemon --exitcrash index.js", "dev": "cross-env NODE_ENV=development nodemon --exitcrash index.js",
@ -11,31 +11,36 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://gitlab.com/ctf-tech-2023/backend-template.git" "url": "git+https://github.com/20kaushik02/express-sequelize-backend-template.git"
}, },
"author": "Kaushik Ravishankar <rknarayan02@gmail.com>", "author": "Kaushik Ravishankar <rknarayan02@gmail.com>",
"license": "ISC", "license": "MIT",
"bugs": { "bugs": {
"url": "https://gitlab.com/ctf-tech-2023/backend-template/issues" "url": "https://github.com/20kaushik02/express-sequelize-backend-template/issues"
}, },
"homepage": "https://gitlab.com/ctf-tech-2023/backend-template#readme", "homepage": "https://github.com/20kaushik02/express-sequelize-backend-template#readme",
"dependencies": { "dependencies": {
"archiver": "^7.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-fetch": "^3.1.5", "cross-fetch": "^4.0.0",
"dotenv-flow": "^3.2.0", "dotenv-flow": "^4.1.0",
"express": "^4.18.1", "express": "^4.18.2",
"express-validator": "^6.14.2", "express-validator": "^7.2.0",
"fast-csv": "^4.3.6", "fast-csv": "^5.0.1",
"helmet": "^6.0.0", "helmet": "^7.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.9.14",
"pg": "^8.8.0", "pg": "^8.12.0",
"sequelize": "^6.24.0", "qrcode": "^1.5.4",
"winston": "^3.8.2" "sequelize": "^6.37.3",
"winston": "^3.14.1"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.2.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"nodemon": "^2.0.20", "nodemon": "^3.1.4",
"sequelize-cli": "^6.5.1" "sequelize-cli": "^6.6.2",
"typescript": "^5.5.4"
} }
} }

1
routes/README.md Normal file
View File

@ -0,0 +1 @@
# Routes - define control flow of the API route

1
seeders/README.md Normal file
View File

@ -0,0 +1 @@
# Sequelize seeder scripts - initial data feed, for dummy data and testing

View File

@ -6,12 +6,6 @@
* @typedef {import("express").Request} Req * @typedef {import("express").Request} Req
* @typedef {import("express").Response} Res * @typedef {import("express").Response} Res
* @typedef {import("express").NextFunction} Next * @typedef {import("express").NextFunction} Next
*
* @typedef {import("sequelize") Sequelize}
* @typedef {import("sequelize").Model Model}
* @typedef {import("sequelize").QueryInterface QueryInterface}
*
* @typedef {import("winston").Logger} Logger
*/ */
exports.unused = {}; exports.unused = {};

1
utils/README.md Normal file
View File

@ -0,0 +1 @@
## General, reusable functionalities unrelated to API

27
utils/archiver.js Normal file
View File

@ -0,0 +1,27 @@
const fs = require("fs");
const archiver = require('archiver');
/**
* @param {string} sourceDir /some/folder/to/compress
* @param {string} outPath /path/to/created.zip
* @returns {Promise}
*/
function zipDirectory(sourceDir, outPath) {
const archive = archiver('zip', { zlib: { level: 9 } });
const stream = fs.createWriteStream(outPath);
return new Promise((resolve, reject) => {
archive
.directory(sourceDir, false)
.on('error', err => reject(err))
.pipe(stream)
;
stream.on('close', () => resolve());
archive.finalize();
});
}
module.exports = {
zipDirectory,
};

14
utils/dateFormatter.js Normal file
View File

@ -0,0 +1,14 @@
/**
* Returns a timestamp string to use for timestamped files
* @returns {string} String of current datetime in YYYYMMDDHHMMSS format
*/
const dateForFilename = () => {
return new Date().
toISOString().slice(-24).
replace(/\D/g, '').
slice(0, 14);
}
module.exports = {
dateForFilename,
};

35
utils/formData.js Normal file
View File

@ -0,0 +1,35 @@
/**
* Recursively build a FormData object from a JSON object
* @param {FormData} formData
* @param {any} data
* @param {string} parentKey
*/
function buildFormData(formData, data, parentKey) {
if (data && typeof data === 'object' && !(data instanceof Date)) {
Object.keys(data).forEach(key => {
buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key);
});
} else {
const value = data == null ? '' : data;
formData.append(parentKey, value);
}
}
/**
* Converts a JSON object to a FormData object
* @param {any} data
* @returns {FormData}
*/
function jsonToFormData(data) {
const formData = new FormData();
buildFormData(formData, data);
return formData;
}
module.exports = {
jsonToFormData,
buildFormData,
};

View File

@ -1,11 +1,11 @@
/** /**
* String joins all the values of a JSON object, including nested keys * Stringifies only values of a JSON object, including nested ones
* *
* @param {any} obj JSON object * @param {any} obj JSON object
* @param {string} delimiter Delimiter of final string * @param {string} delimiter Delimiter of final string
* @returns * @returns {string}
*/ */
const getNestedValuesString = (obj, delimiter) => { const getNestedValuesString = (obj, delimiter = ', ') => {
let values = []; let values = [];
for (key in obj) { for (key in obj) {
if (typeof obj[key] !== "object") { if (typeof obj[key] !== "object") {
@ -15,9 +15,9 @@ const getNestedValuesString = (obj, delimiter) => {
} }
} }
return delimiter ? values.join(delimiter) : values.join(); return values.join(delimiter);
} }
module.exports = { module.exports = {
getNestedValuesString getNestedValuesString
} };

View File

@ -1,37 +1,34 @@
// Whole thing is winston logger stuff, if you want to learn read the docs
const path = require("path"); const path = require("path");
const { createLogger, transports, config, format } = require("winston"); const { createLogger, transports, config, format } = require('winston');
const { combine, label, timestamp, printf } = format; const { combine, label, timestamp, printf, errors } = format;
const typedefs = require("../typedefs"); const typedefs = require("../typedefs");
const getLabel = (callingModule) => { const getLabel = (callingModule) => {
const parts = callingModule.filename.split(path.sep); if (!callingModule.filename) return "repl";
const parts = callingModule.filename?.split(path.sep);
return path.join(parts[parts.length - 2], parts.pop()); return path.join(parts[parts.length - 2], parts.pop());
}; };
const logMetaReplacer = (key, value) => { const allowedErrorKeys = ["name", "code", "message", "stack"];
if (key === "error") {
return value.name + ": " + value.message;
}
return value;
}
const metaFormat = (meta) => { const metaFormat = (meta) => {
if (Object.keys(meta).length > 0) if (Object.keys(meta).length > 0)
return "\n" + JSON.stringify(meta, logMetaReplacer) + "\n"; return '\n' + JSON.stringify(meta, null, "\t");
return "\n"; return '';
} }
const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
if (meta.error) { if (meta.error) { // if the error was passed
for (const key in meta.error) { for (const key in meta.error) {
if (typeof key !== "symbol" && key !== "message" && key !== "name") { if (!allowedErrorKeys.includes(key)) {
delete meta.error[key] delete meta.error[key];
} }
} }
const { stack, ...rest } = meta.error;
return `${timestamp} [${label}] ${level}: ${message}${metaFormat(rest)}\n` +
`${stack ?? ''}`;
} }
return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`; return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`;
}); });
@ -39,22 +36,32 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
/** /**
* Creates a curried function, and call it with the module in use to get logs with filename * Creates a curried function, and call it with the module in use to get logs with filename
* @param {typedefs.Module} callingModule The module from which the logger is called * @param {typedefs.Module} callingModule The module from which the logger is called
* @returns {typedefs.Logger}
*/ */
const logger = (callingModule) => { const curriedLogger = (callingModule) => {
return createLogger({ let winstonLogger = createLogger({
levels: config.npm.levels, levels: config.npm.levels,
format: combine( format: combine(
errors({ stack: true }),
label({ label: getLabel(callingModule) }), label({ label: getLabel(callingModule) }),
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat, logFormat,
), ),
transports: [ transports: [
new transports.Console(), new transports.Console({ level: 'info' }),
new transports.File({ filename: __dirname + "/../logs/common.log" }), new transports.File({
new transports.File({ filename: __dirname + "/../logs/error.log", level: "error" }), filename: __dirname + '/../logs/debug.log',
level: 'debug',
maxsize: 10485760,
}),
new transports.File({
filename: __dirname + '/../logs/error.log',
level: 'error',
maxsize: 1048576,
}),
] ]
}); });
winstonLogger.on('error', (error) => winstonLogger.error("Error inside logger", { error }));
return winstonLogger;
} }
module.exports = logger; module.exports = curriedLogger;

View File

@ -1,45 +0,0 @@
const mailer = require("nodemailer");
const logger = require("./logger")(module);
// Creates a mailer transporter object with authentication and base config
const transport = mailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
service: "gmail",
auth: {
user: process.env.AUTOMAILER_ID,
pass: process.env.AUTOMAILER_APP_PASSWD,
}
});
/**
* Sends a mail from web user to a mail inside organization
* @param {string} mailTarget Target mail - must be within organization
* @param {string} mailSubject Mail subject
* @param {{name: string, email: string, message: string}} userData User details: name, email, and message
*/
const inboundMailer = (mailTarget, mailSubject, userData) => {
if (!mailTarget.endsWith("cegtechforum.in")) {
throw new Error("Invalid target mail domain.");
}
const message = {
to: mailTarget,
subject: mailSubject,
html:
"<p>Name: " + userData.name + "</p><p>Email: " + userData.email + "</p><br/><p>Message:<br/>" + userData.message + "</p>"
};
transport.sendMail(message, (err, info) => {
if (err) {
logger.error("Failure: QUERY mail NOT sent", { err, userData });
} else {
logger.info("Success: QUERY mail sent", { info });
}
});
};
module.exports = {
inboundMailer
}

30
utils/sendAttachment.js Normal file
View File

@ -0,0 +1,30 @@
const typedefs = require("../typedefs");
const fastCSV = require("fast-csv");
const stream = require("stream");
/**
* Sends object data from Sequelize after formatting into CSV
*
* @param {typedefs.Res} res Express response object
* @param {string} filename Filename for attachment. Prefer timestamped names
* @param {any[]} data Data from database queries, without metadata
* @returns
*/
const sendCSV = async (res, filename, data) => {
const csvData = await fastCSV.writeToBuffer(data, { headers: true });
// refer https://stackoverflow.com/a/45922316/
const fileStream = new stream.PassThrough();
fileStream.end(csvData);
res.attachment(filename + ".csv");
res.type("text/csv");
fileStream.pipe(res);
return;
}
module.exports = {
sendCSV,
}

48
utils/token.js Normal file
View File

@ -0,0 +1,48 @@
const fs = require("fs");
const jwt = require("jsonwebtoken");
const privateKey = fs.readFileSync(process.env.PRIVKEY_PATH);
const publicKey = fs.readFileSync(process.env.PUBKEY_PATH);
/**
* Sign data into JWT with JWT env secret
* @param {string|any} data
* @returns {jwt.JwtPayload}
*/
const getJWT = (data) => {
return jwt.sign({ id: data }, process.env.JWTSECRET, { algorithm: "HS256" }); // symmetric encryption, so simple secret with SHA
};
/**
* Sign data into JWT with private key.
* @param {string|any} data
* @returns {jwt.JwtPayload}
*/
const getSignedJWT = (data) => {
return jwt.sign({ id: data }, privateKey, { algorithm: "RS256" }); // asymmetric signing, so private key with RSA
}
/**
* Verify a JWT with JWT env secret
* @param {jwt.JwtPayload} data
* @returns {string|any}
*/
const verifyJWT = (data) => {
return jwt.verify(data, process.env.JWTSECRET, { algorithms: ["HS256"] });
}
/**
* Verify a signed JWT with public key.
* @param {jwt.JwtPayload} signedString
* @returns {string|any}
*/
const verifySignedJWT = (signedString) => {
return jwt.verify(signedString, publicKey, { algorithms: ["RS256"] });
}
module.exports = {
getJWT,
verifyJWT,
getSignedJWT,
verifySignedJWT,
};

1
validators/README.md Normal file
View File

@ -0,0 +1 @@
## Validators - middleware functions for request object validation

View File

@ -1,7 +1,9 @@
const { validationResult } = require("express-validator"); const { validationResult } = require("express-validator");
const typedefs = require("../typedefs");
const { getNestedValuesString } = require("../utils/jsonTransformer"); const { getNestedValuesString } = require("../utils/jsonTransformer");
const logger = require("../utils/logger")(module);
const typedefs = require("../typedefs");
/** /**
* Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator * Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator
@ -15,17 +17,30 @@ const validate = (req, res, next) => {
if (errors.isEmpty()) { if (errors.isEmpty()) {
return next(); return next();
} }
const extractedErrors = []
errors.array().map(err => extractedErrors.push({
[err.param]: err.msg
}));
return res.status(400).send({ const extractedErrors = [];
errors.array().forEach(err => {
if (err.type === 'alternative') {
err.nestedErrors.forEach(nestedErr => {
extractedErrors.push({
[nestedErr.path]: nestedErr.msg
});
});
} else if (err.type === 'field') {
extractedErrors.push({
[err.path]: err.msg
});
}
});
res.status(400).json({
message: getNestedValuesString(extractedErrors), message: getNestedValuesString(extractedErrors),
errors: extractedErrors errors: extractedErrors
}) });
logger.warn("invalid request", { extractedErrors });
return;
} }
module.exports = { module.exports = {
validate, validate
} };