Compare commits

..

No commits in common. "master" and "v1.1.0" have entirely different histories.

38 changed files with 4651 additions and 616 deletions

12
.env
View File

@ -1,12 +0,0 @@
PORT=5000
TRUST_PROXY = 1
AUTOMAILER_SMTP_SERVICE
AUTOMAILER_SMTP_HOST
AUTOMAILER_ID = "mailerID@mailserver.domain"
AUTOMAILER_APP_PASSWD = "mailerpasswd"
JWTSECRET="somerandomstring"
ADMIN_USER = "ADMIN_USER"
ADMIN_PASSWD = "SOME_ADMIN_PASSWD"

View File

@ -1,8 +0,0 @@
DB_USER = your_database_username
DB_PASSWD = your_database_password
DB_NAME = your_database_name
DB_HOST = localhost
DB_PORT = your_database_port
DB_DIALECT = your_database_dialect
CAPTCHA_SECRET = 'your-captcha-secret'

View File

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

99
.gitignore vendored
View File

@ -6,101 +6,14 @@ 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/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.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 # dotenv environment variables file
.env.local .env.development
.env.*.local .env.staging
.env.production
*.env
# parcel-bundler cache (https://parceljs.org/) # Data files
.cache */*.csv
# 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", "sequelize.js") "config": path.resolve("config", "config.js")
}; };

21
LICENSE
View File

@ -1,21 +0,0 @@
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,17 +1 @@
# Express-Sequelize backend server template # Induction app back-end
## To get started
- 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 `README.md` to reflect project details
- 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
- [See here](https://github.com/kerimdzhanov/dotenv-flow?tab=readme-ov-file#files-under-version-control) for best practices for .env files configuration
- Staging: `npm run staging_prep` and `npm run staging` to deploy on Render after configuring a new web service on Render dashboard

View File

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

View File

@ -1,21 +1,16 @@
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. * @param {typedefs.Req} req
* Everything should be contained inside try-catch blocks * @param {typedefs.Res} res
*
* @param {typedefs.Req} req Express request object
* @param {typedefs.Res} res Express response object
*/ */
const __controller_func = async (req, res) => { const __controller_func = async (req, res) => {
try { try {
} catch (error) { } catch (error) {
res.sendStatus(500); logger.error("Error", { error });
logger.error("__controller_func", { error }); return res.status(500).send({ message: "Server Error. Try again." });
return;
} }
} }

View File

@ -3,15 +3,11 @@ const router = require("express").Router();
const { validate } = require("../validators"); const { validate } = require("../validators");
router.get( router.get(
// URL,
// middleware,
// validators,
// validate,
// controller
); );
router.post( router.post(
//similar
); );
module.exports = router; module.exports = router;

View File

@ -3,12 +3,6 @@ const { body, header, param, query } = require("express-validator");
const typedefs = require("../typedefs"); const typedefs = require("../typedefs");
/** /**
* Validator middleware function
*
* Use the necessary part of the request, such as params for URL parameters, or query for query parameters.
*
* Refer https://github.com/validatorjs/validator.js for a full list of the validators and sanitizers available.
*
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
* @param {typedefs.Next} next * @param {typedefs.Next} next
@ -24,4 +18,5 @@ const __validator_func = async (req, res, next) => {
module.exports = { module.exports = {
__validator_func, __validator_func,
}; }

View File

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

23
config/config.js Normal file
View File

@ -0,0 +1,23 @@
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,
},
}
}

View File

@ -1,28 +0,0 @@
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;

View File

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

View File

@ -1,35 +1,21 @@
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();
// Enable this if you run behind a proxy (e.g. nginx) app.use(express.json());
app.set('trust proxy', process.env.TRUST_PROXY); app.use(express.urlencoded({ extended: true }));
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((_req, res) => {
app.use(express.urlencoded({ extended: true })); return res.status(200).send("Induction app back-end.");
// Static
app.use(express.static(__dirname + '/static'));
// Put routes here
// Fallbacks
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;
@ -37,20 +23,3 @@ 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));
});

View File

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

View File

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

View File

@ -1,37 +0,0 @@
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,
};

View File

@ -1,39 +0,0 @@
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
};

View File

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

View File

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

View File

@ -2,45 +2,36 @@
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/sequelize.js")[env]; const config = require(__dirname + "/../config/config.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);
} else { } else {
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)
.filter(file => { .filter(file => {
return (file.indexOf(".") !== 0) && (file !== basename) && (file.slice(-3) === ".js"); return (file.indexOf(".") !== 0) && (file !== basename) && (file.slice(-3) === ".js");
}) })
.forEach(file => { .forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model; db[model.name] = model;
}); });
// Setup defined associations // Setup defined associations
Object.keys(db).forEach(modelName => { Object.keys(db).forEach(modelName => {
if (db[modelName].associate) { if (db[modelName].associate) {
db[modelName].associate(db); db[modelName].associate(db);
} }
}); });
db.sequelize = sequelize; db.sequelize = sequelize;

4514
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,11 +1,15 @@
// Type definitions to enable better auto-complete features.
/** /**
* @typedef {import("module")} Module * @typedef {import("module")} Module
* *
* @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 = {};

View File

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

View File

@ -1,27 +0,0 @@
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,
};

View File

@ -1,14 +0,0 @@
/**
* 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,
};

View File

@ -1,35 +0,0 @@
/**
* 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,4 @@
/** function getNestedValuesString(obj) {
* Stringifies only values of a JSON object, including nested ones
*
* @param {any} obj JSON object
* @param {string} delimiter Delimiter of final string
* @returns {string}
*/
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 +8,9 @@ const getNestedValuesString = (obj, delimiter = ', ') => {
} }
} }
return values.join(delimiter); return values.join();
} }
module.exports = { module.exports = {
getNestedValuesString getNestedValuesString
}; }

View File

@ -1,67 +1,58 @@
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, errors } = format; const { combine, label, timestamp, printf } = format;
const typedefs = require("../typedefs"); const typedefs = require("../typedefs");
const getLabel = (callingModule) => { const getLabel = (callingModule) => {
if (!callingModule.filename) return "repl"; const parts = callingModule.filename.split(path.sep);
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 allowedErrorKeys = ["name", "code", "message", "stack"]; const logMetaReplacer = (key, value) => {
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, null, "\t"); return "\n" + JSON.stringify(meta, logMetaReplacer) + "\n";
return ''; return "\n";
} }
const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
if (meta.error) { // if the error was passed if (meta.error) {
for (const key in meta.error) { for (const key in meta.error) {
if (!allowedErrorKeys.includes(key)) { if (typeof key !== "symbol" && key !== "message" && key !== "name") {
delete meta.error[key]; delete meta.error[key]
} }
} }
const { stack, ...rest } = meta.error; }
return `${timestamp} [${label}] ${level}: ${message}${metaFormat(rest)}\n` + return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`;
`${stack ?? ''}`;
}
return `${timestamp} [${label}] ${level}: ${message}${metaFormat(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 curriedLogger = (callingModule) => { const logger = (callingModule) => {
let winstonLogger = createLogger({ return 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 = curriedLogger; module.exports = logger;

View File

@ -1,30 +0,0 @@
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,
}

View File

@ -1,48 +0,0 @@
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,
};

View File

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

View File

@ -1,9 +1,7 @@
const { validationResult } = require("express-validator"); const { validationResult } = require("express-validator");
const { getNestedValuesString } = require("../utils/jsonTransformer");
const logger = require("../utils/logger")(module);
const typedefs = require("../typedefs"); const typedefs = require("../typedefs");
const { getNestedValuesString } = require("../utils/jsonTransformer");
/** /**
* Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator * Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator
@ -17,30 +15,17 @@ 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
}));
const extractedErrors = []; return res.status(400).send({
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,
}; }