diff --git a/middleware/admin.js b/middleware/admin.js new file mode 100644 index 0000000..5e228aa --- /dev/null +++ b/middleware/admin.js @@ -0,0 +1,34 @@ +const typedefs = require("../typedefs"); +const logger = require("../utils/logger")(module); + +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 {JSON} */ + 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 + logger.warn("Intruder alert.", { ip: unauthIP }); + return res.status(401).send("Intruder alert. IP address: " + unauthIP); + } + } catch (error) { + logger.error("adminQueryCreds", { error }); + return res.status(500).send({ message: "Server Error. Try again." }); + } +} + +module.exports = { + adminQueryCreds, +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0d77d5b..3a153f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend-template", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "backend-template", - "version": "1.3.0", + "version": "1.3.1", "license": "ISC", "dependencies": { "cors": "^2.8.5", @@ -16,7 +16,7 @@ "express-validator": "^6.14.2", "fast-csv": "^4.3.6", "helmet": "^6.0.0", - "jsonwebtoken": "^8.5.1", + "jsonwebtoken": "^9.0.0", "nodemailer": "^6.8.0", "pg": "^8.8.0", "sequelize": "^6.24.0", @@ -1160,24 +1160,29 @@ } }, "node_modules/jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "dependencies": { "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" + "semver": "^7.3.8" }, "engines": { - "node": ">=4", - "npm": ">=1.4.28" + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/jsonwebtoken/node_modules/ms": { @@ -1185,6 +1190,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonwebtoken/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -1224,11 +1248,6 @@ "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -1244,41 +1263,16 @@ "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, "node_modules/lodash.isnil": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "node_modules/lodash.isundefined": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -1847,9 +1841,9 @@ } }, "node_modules/retry-as-promised": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz", - "integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA==" + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" }, "node_modules/safe-buffer": { "version": "5.2.1", @@ -1887,6 +1881,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, "bin": { "semver": "bin/semver" } @@ -1920,9 +1915,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/sequelize": { - "version": "6.25.4", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.25.4.tgz", - "integrity": "sha512-gD6RoEVlTKxQh9GC6Qd+pW2A9uhmqeuwBHGMGcZtDg2CchXQhrwS+k6ZnWZ1ggYZ3tUkk4q8fkFL3/XmXnMFFQ==", + "version": "6.31.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.31.1.tgz", + "integrity": "sha512-cahWtRrYLjqoZP/aurGBoaxn29qQCF4bxkAUPEQ/ozjJjt6mtL4Q113S3N39mQRmX5fgxRbli+bzZARP/N51eg==", "funding": [ { "type": "opencollective", @@ -1937,9 +1932,9 @@ "inflection": "^1.13.2", "lodash": "^4.17.21", "moment": "^2.29.1", - "moment-timezone": "^0.5.34", + "moment-timezone": "^0.5.35", "pg-connection-string": "^2.5.0", - "retry-as-promised": "^6.1.0", + "retry-as-promised": "^7.0.3", "semver": "^7.3.5", "sequelize-pool": "^7.1.0", "toposort-class": "^1.0.1", @@ -3462,26 +3457,41 @@ } }, "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "requires": { "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" + "semver": "^7.3.8" }, "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -3524,11 +3534,6 @@ "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, "lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -3544,41 +3549,16 @@ "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, "lodash.isnil": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "lodash.isundefined": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -4006,9 +3986,9 @@ } }, "retry-as-promised": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz", - "integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA==" + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" }, "safe-buffer": { "version": "5.2.1", @@ -4028,7 +4008,8 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true }, "send": { "version": "0.18.0", @@ -4058,9 +4039,9 @@ } }, "sequelize": { - "version": "6.25.4", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.25.4.tgz", - "integrity": "sha512-gD6RoEVlTKxQh9GC6Qd+pW2A9uhmqeuwBHGMGcZtDg2CchXQhrwS+k6ZnWZ1ggYZ3tUkk4q8fkFL3/XmXnMFFQ==", + "version": "6.31.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.31.1.tgz", + "integrity": "sha512-cahWtRrYLjqoZP/aurGBoaxn29qQCF4bxkAUPEQ/ozjJjt6mtL4Q113S3N39mQRmX5fgxRbli+bzZARP/N51eg==", "requires": { "@types/debug": "^4.1.7", "@types/validator": "^13.7.1", @@ -4069,9 +4050,9 @@ "inflection": "^1.13.2", "lodash": "^4.17.21", "moment": "^2.29.1", - "moment-timezone": "^0.5.34", + "moment-timezone": "^0.5.35", "pg-connection-string": "^2.5.0", - "retry-as-promised": "^6.1.0", + "retry-as-promised": "^7.0.3", "semver": "^7.3.5", "sequelize-pool": "^7.1.0", "toposort-class": "^1.0.1", diff --git a/package.json b/package.json index ba8b439..5958677 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "express-validator": "^6.14.2", "fast-csv": "^4.3.6", "helmet": "^6.0.0", - "jsonwebtoken": "^8.5.1", + "jsonwebtoken": "^9.0.0", "nodemailer": "^6.8.0", "pg": "^8.8.0", "sequelize": "^6.24.0", diff --git a/typedefs.js b/typedefs.js index d0f1fbf..52fb5ca 100644 --- a/typedefs.js +++ b/typedefs.js @@ -7,9 +7,9 @@ * @typedef {import("express").Response} Res * @typedef {import("express").NextFunction} Next * - * @typedef {import("sequelize") Sequelize} - * @typedef {import("sequelize").Model Model} - * @typedef {import("sequelize").QueryInterface QueryInterface} + * @typedef {import("sequelize")} Sequelize + * @typedef {import("sequelize").Model} Model + * @typedef {import("sequelize").QueryInterface} QueryInterface * * @typedef {import("winston").Logger} Logger */ diff --git a/utils/archiver.js b/utils/archiver.js new file mode 100644 index 0000000..cc4b112 --- /dev/null +++ b/utils/archiver.js @@ -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, +} \ No newline at end of file diff --git a/utils/dateFormatter.js b/utils/dateFormatter.js new file mode 100644 index 0000000..2e5b851 --- /dev/null +++ b/utils/dateFormatter.js @@ -0,0 +1,12 @@ +/** + * Returns a timestamp string to use for timestamped files + * @returns {string} String of current datetime in YYYY.MM.DD-HH:MM:SS format + */ +const dateForFilename = () => { + const dt = new Date(); + return `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}-${dt.getHours()}-${dt.getMinutes()}-${dt.getSeconds()}`; +} + +module.exports = { + dateForFilename, +} \ No newline at end of file diff --git a/utils/formData.js b/utils/formData.js new file mode 100644 index 0000000..35379fd --- /dev/null +++ b/utils/formData.js @@ -0,0 +1,24 @@ +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); + } +} + +function jsonToFormData(data) { + const formData = new FormData(); + + buildFormData(formData, data); + + return formData; +} + +module.exports = { + jsonToFormData, + buildFormData, +} \ No newline at end of file diff --git a/utils/qrGenerator.js b/utils/qrGenerator.js new file mode 100644 index 0000000..ff2943c --- /dev/null +++ b/utils/qrGenerator.js @@ -0,0 +1,58 @@ +const pathLib = require("path"); +const qr = require("qrcode"); +const logger = require("./logger")(module); +const { getSignedJWT } = require("./token"); + +/** + * Generates QR code from data and writes to file in tmp folder. + * To avoid race conditions, use email or other unique attributes for id. + * @param {string|any} data String or JSON object + */ +const qrPNGFile = (id, data) => { + qr.toFile( + path = pathLib.join(__dirname, "../tmp/tmpQR-" + id + ".png"), + text = (typeof data === "object" ? JSON.stringify(data) : data), + options = { type: 'png' }, + (err) => { + if (err) { + logger.error("qrPNGFile", err); + throw err; + } + } + ); +} + +/** + * Generates QR code from data after signing and writes to file in tmp or k-qrs folder. + * + * To avoid race conditions, use email or other unique attributes for ID. + * @param {string|any} data String or JSON object + */ +const qrSignedPNGFile = (id, data, tmp = true) => { + const signedData = getSignedJWT(data); + const qrFilename = `${tmp ? 'tmpEncQR' : 'K-QR'}-${id}.png`; + const targetPath = pathLib.join( + __dirname, "..", + tmp ? "tmp" : pathLib.join("uploads", "2023", "k-qrs"), + qrFilename, + ); + + qr.toFile( + path = targetPath, + text = (typeof data === "object" ? JSON.stringify(signedData) : signedData), + options = { type: 'png' }, + (err) => { + if (err) { + logger.error("qrSignedPNGFile", err); + throw err; + } + } + ) + + return qrFilename; +} + +module.exports = { + qrPNGFile, + qrSignedPNGFile, +} \ No newline at end of file diff --git a/utils/quickEncrypt.js b/utils/quickEncrypt.js new file mode 100644 index 0000000..d2aceb4 --- /dev/null +++ b/utils/quickEncrypt.js @@ -0,0 +1,23 @@ +/* Taken from quick-encrypt package, which is not maintained anymore */ + +const crypto = require('crypto') + +const acceptableBitSizes = [1024, 2048]; + +exports.generate = (sizeInBits) => { + if (!acceptableBitSizes.includes(sizeInBits)) + throw Error('Error generating public and private key. Key size can only be 1024 or 2048. Example usage: ` let keys = QuickEncrypt.generate(2048); `') + return keypair({ bits: sizeInBits }) +} + +exports.encrypt = (payloadString, publicKey) => { + if (typeof payloadString !== 'string' || typeof publicKey !== 'string') + throw Error("Error encrypting. Payload and Public Key should be in text format. Example usage: ` let encryptedText = QuickEncrypt.encrypt('Some secret text here!', 'the public RSA key in text format here'); ` ") + return crypto.publicEncrypt(publicKey, Buffer.from(payloadString, 'utf8')).toString('hex') +} + +exports.decrypt = (encryptedString, privateKey) => { + if (typeof encryptedString !== 'string' || typeof privateKey !== 'string') + throw Error("Error decrypting. Decrypted Text and Private Key should be in text format. Example usage: ` let decryptedText = QuickEncrypt.decrypt('asddd213d19jenacanscasn', 'the private RSA key in text format here'); ` ") + return crypto.privateDecrypt({ key: privateKey }, Buffer.from(encryptedString, 'hex')).toString() +} diff --git a/utils/sendAttachment.js b/utils/sendAttachment.js new file mode 100644 index 0000000..808da05 --- /dev/null +++ b/utils/sendAttachment.js @@ -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, +} \ No newline at end of file diff --git a/utils/token.js b/utils/token.js new file mode 100644 index 0000000..c4bab1b --- /dev/null +++ b/utils/token.js @@ -0,0 +1,60 @@ +const fs = require("fs"); +const jwt = require("jsonwebtoken"); + +const privateKey = fs.readFileSync(process.env.PRIVKEY); +const publicKey = fs.readFileSync(process.env.PUBKEY); + +/** + * 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, +};