From 08615ff590e2990149e63ed5f18f5ca235880c35 Mon Sep 17 00:00:00 2001 From: Ayobami Date: Tue, 12 Aug 2025 17:41:06 +0100 Subject: [PATCH] fix: code review fixes --- app.js | 45 ++- package-lock.json | 691 ++++++++++++++++++++++++++++++++++ package.json | 3 + public/main.js | 254 +++++++++++++ public/stylesheets/output.css | 142 +++++++ routes/index.js | 169 ++++++++- views/index.html | 101 +++++ 7 files changed, 1389 insertions(+), 16 deletions(-) diff --git a/app.js b/app.js index 3f11a82..781685f 100644 --- a/app.js +++ b/app.js @@ -1,26 +1,41 @@ -var createError = require('http-errors'); -var express = require('express'); -var path = require('path'); -var cookieParser = require('cookie-parser'); -var logger = require('morgan'); +var createError = require("http-errors"); +var express = require("express"); +var path = require("path"); +var cookieParser = require("cookie-parser"); +var logger = require("morgan"); +var session = require("express-session"); -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); +var indexRouter = require("./routes/index"); +var usersRouter = require("./routes/users"); var app = express(); // view engine setup -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'pug'); +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "pug"); -app.use(logger('dev')); +app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); -app.use('/', indexRouter); -app.use('/users', usersRouter); +// Session middleware for 2FA +app.use( + session({ + secret: + process.env.SESSION_SECRET || "your-secret-key-change-in-production", + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === "production", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }, + }) +); +app.use(express.static(path.join(__dirname, "public"))); + +app.use("/", indexRouter); +app.use("/users", usersRouter); // catch 404 and forward to error handler app.use(function (req, res, next) { @@ -31,11 +46,11 @@ app.use(function (req, res, next) { app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + res.locals.error = req.app.get("env") === "development" ? err : {}; // render the error page res.status(err.status || 500); - res.render('error'); + res.render("error"); }); module.exports = app; diff --git a/package-lock.json b/package-lock.json index a054deb..423b088 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", + "express-session": "^1.17.3", "http-errors": "~1.6.3", "mariadb": "^3.0.1", "morgan": "~1.9.1", @@ -21,8 +22,10 @@ "node-fetch": "^2.7.0", "ol": "^7.1.0", "pug": "^3.0.2", + "qrcode": "^1.5.3", "redis": "^5.6.1", "sequelize": "^6.21.6", + "speakeasy": "^2.0.0", "stripe": "^18.3.0", "xmlbuilder2": "^3.1.1" }, @@ -921,6 +924,30 @@ "node": ">= 0.6" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -995,6 +1022,12 @@ "node": ">= 10.0.0" } }, + "node_modules/base32.js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", + "integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ==", + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -1137,6 +1170,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001727", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", @@ -1167,6 +1209,17 @@ "node": ">=18" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -1176,6 +1229,24 @@ "node": ">=0.10.0" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -1245,6 +1316,15 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -1275,6 +1355,12 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -1316,6 +1402,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -1443,6 +1535,78 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express-session/node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express-session/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/express/node_modules/cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -1480,6 +1644,19 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1545,6 +1722,15 @@ "node": ">=10.19" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1754,6 +1940,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2065,6 +2260,18 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2509,6 +2716,42 @@ "node": ">= 0.8" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pako": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", @@ -2527,6 +2770,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2580,6 +2832,15 @@ "semver-compare": "^1.0.0" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2828,6 +3089,23 @@ "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -2841,6 +3119,15 @@ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2901,6 +3188,21 @@ "node": ">= 18" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -3093,6 +3395,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", @@ -3207,6 +3515,18 @@ "node": ">=0.10.0" } }, + "node_modules/speakeasy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", + "integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==", + "license": "MIT", + "dependencies": { + "base32.js": "0.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3266,6 +3586,32 @@ ], "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stripe": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.3.0.tgz", @@ -3407,6 +3753,18 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3505,6 +3863,12 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", @@ -3513,6 +3877,20 @@ "@types/node": "*" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/xml-utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.2.0.tgz", @@ -3542,10 +3920,51 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "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/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } } }, "dependencies": { @@ -4010,6 +4429,19 @@ "negotiator": "0.6.3" } }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, "append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -4055,6 +4487,11 @@ "@babel/types": "^7.9.6" } }, + "base32.js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", + "integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -4145,6 +4582,11 @@ "get-intrinsic": "^1.3.0" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "caniuse-lite": { "version": "1.0.30001727", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", @@ -4156,11 +4598,34 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -4214,6 +4679,11 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, "denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -4234,6 +4704,11 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==" }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -4270,6 +4745,11 @@ "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", "dev": true }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -4367,6 +4847,48 @@ } } }, + "express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "requires": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4389,6 +4911,15 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4432,6 +4963,11 @@ "xml-utils": "^1.0.2" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4565,6 +5101,11 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4704,6 +5245,14 @@ "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "optional": true }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -5021,6 +5570,27 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "pako": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", @@ -5036,6 +5606,11 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -5078,6 +5653,11 @@ "semver-compare": "^1.0.0" } }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5306,6 +5886,16 @@ "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" }, + "qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "requires": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + } + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -5316,6 +5906,11 @@ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5362,6 +5957,16 @@ "@redis/time-series": "5.6.1" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -5492,6 +6097,11 @@ "send": "0.16.2" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", @@ -5565,6 +6175,14 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, + "speakeasy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", + "integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==", + "requires": { + "base32.js": "0.0.1" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5600,6 +6218,24 @@ } } }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, "stripe": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.3.0.tgz", @@ -5695,6 +6331,14 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5754,6 +6398,11 @@ "webidl-conversions": "^3.0.0" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", @@ -5762,6 +6411,16 @@ "@types/node": "*" } }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "xml-utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.2.0.tgz", @@ -5783,10 +6442,42 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } } diff --git a/package.json b/package.json index 9321583..3bead21 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", + "express-session": "^1.17.3", "http-errors": "~1.6.3", "mariadb": "^3.0.1", "morgan": "~1.9.1", @@ -25,6 +26,8 @@ "redis": "^5.6.1", "sequelize": "^6.21.6", "stripe": "^18.3.0", + "speakeasy": "^2.0.0", + "qrcode": "^1.5.3", "xmlbuilder2": "^3.1.1" }, "devDependencies": { diff --git a/public/main.js b/public/main.js index 7bdad4d..c2d0687 100644 --- a/public/main.js +++ b/public/main.js @@ -1,5 +1,204 @@ // main.js +// --- 2FA Implementation --- +let twoFactorVerified = false; + +// Check 2FA status on page load +async function check2FAStatus() { + try { + const res = await fetch("/2fa/status"); + if (res.ok) { + const data = await res.json(); + twoFactorVerified = data.verified; + if (!twoFactorVerified) { + show2FAModal(); + } + } else { + show2FAModal(); + } + } catch (err) { + show2FAModal(); + } +} + +// Show 2FA modal +function show2FAModal() { + const modal = document.getElementById("2fa-modal"); + const setup = document.getElementById("2fa-setup"); + const verify = document.getElementById("2fa-verify"); + const loading = document.getElementById("2fa-loading"); + + modal.classList.remove("hidden"); + loading.classList.remove("hidden"); + setup.classList.add("hidden"); + verify.classList.add("hidden"); + + // Generate 2FA secret + generate2FASecret(); +} + +// Generate 2FA secret +async function generate2FASecret() { + try { + const res = await fetch("/2fa/generate"); + if (res.ok) { + const data = await res.json(); + + const setup = document.getElementById("2fa-setup"); + const verify = document.getElementById("2fa-verify"); + const loading = document.getElementById("2fa-loading"); + + loading.classList.add("hidden"); + setup.classList.remove("hidden"); + + // Display secret key + document.getElementById("secret-key").textContent = data.secret; + + // Generate QR code image + const qrCode = document.getElementById("qr-code"); + qrCode.innerHTML = ` +
+ QR Code for 2FA + +

Scan this QR code with your authenticator app

+
+ `; + } else { + throw new Error("Failed to generate 2FA secret"); + } + } catch (err) { + console.error("2FA generation error:", err); + document.getElementById("2fa-loading").innerHTML = + '

Error generating 2FA. Please refresh the page.

'; + } +} + +// Setup 2FA event listeners +document.addEventListener("DOMContentLoaded", function () { + const generateBtn = document.getElementById("generate-2fa"); + const verifyBtn = document.getElementById("verify-2fa"); + const backBtn = document.getElementById("back-to-setup"); + const tokenInput = document.getElementById("2fa-token"); + + if (generateBtn) { + generateBtn.addEventListener("click", generate2FASecret); + } + + if (verifyBtn) { + verifyBtn.addEventListener("click", verify2FA); + } + + if (backBtn) { + backBtn.addEventListener("click", function () { + document.getElementById("2fa-setup").classList.remove("hidden"); + document.getElementById("2fa-verify").classList.add("hidden"); + }); + } + + if (tokenInput) { + tokenInput.addEventListener("input", function () { + if (this.value.length === 6) { + verify2FA(); + } + }); + } + + const proceedBtn = document.getElementById("proceed-to-verify"); + if (proceedBtn) { + proceedBtn.addEventListener("click", function () { + document.getElementById("2fa-setup").classList.add("hidden"); + document.getElementById("2fa-verify").classList.remove("hidden"); + document.getElementById("2fa-token").focus(); + }); + } + + const downloadQrBtn = document.getElementById("download-qr"); + if (downloadQrBtn) { + downloadQrBtn.addEventListener("click", function () { + const qrImg = document.querySelector("#qr-code img"); + if (qrImg && qrImg.src) { + const link = document.createElement("a"); + link.download = "2fa-qr-code.png"; + link.href = qrImg.src; + link.click(); + } + }); + } + + const copySecretBtn = document.getElementById("copy-secret"); + if (copySecretBtn) { + copySecretBtn.addEventListener("click", function () { + const secretKey = document.getElementById("secret-key").textContent; + if (secretKey) { + navigator.clipboard + .writeText(secretKey) + .then(() => { + // Show temporary success message + const originalText = copySecretBtn.textContent; + copySecretBtn.textContent = "Copied!"; + copySecretBtn.classList.remove("bg-gray-500", "hover:bg-gray-600"); + copySecretBtn.classList.add("bg-green-500"); + setTimeout(() => { + copySecretBtn.textContent = originalText; + copySecretBtn.classList.remove("bg-green-500"); + copySecretBtn.classList.add("bg-gray-500", "hover:bg-gray-600"); + }, 2000); + }) + .catch(() => { + alert("Failed to copy to clipboard. Secret: " + secretKey); + }); + } + }); + } +}); + +// Verify 2FA token +async function verify2FA() { + const token = document.getElementById("2fa-token").value.trim(); + + if (token.length !== 6) { + alert("Please enter a 6-digit code"); + return; + } + + try { + const res = await fetch("/2fa/verify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token }), + }); + + if (res.ok) { + const data = await res.json(); + if (data.success) { + twoFactorVerified = true; + document.getElementById("2fa-modal").classList.add("hidden"); + // Enable all widgets + enableDashboard(); + } else { + alert("Invalid token. Please try again."); + } + } else { + const errorData = await res.json(); + alert(errorData.error || "Verification failed"); + } + } catch (err) { + alert("Verification failed. Please try again."); + } +} + +// Enable dashboard after 2FA verification +function enableDashboard() { + // Dashboard is already visible, just ensure all functionality is enabled + console.log("Dashboard enabled after 2FA verification"); +} + +// Check 2FA status on page load +check2FAStatus(); + // Function to update weather widget async function updateWeather() { try { @@ -275,6 +474,61 @@ if (exportBtn && exportBtn.textContent.trim().toLowerCase() === "export") { }); } +// --- Import XML Widget --- +const importXmlInput = document.getElementById("import-xml-input"); +const importXmlBtn = document.getElementById("import-xml-btn"); +const importStatus = document.getElementById("import-status"); + +if (importXmlBtn && importXmlInput && importStatus) { + importXmlBtn.addEventListener("click", async function () { + if (!importXmlInput.files || !importXmlInput.files[0]) { + importStatus.textContent = "Please select an XML file"; + importStatus.className = "text-xs mt-2 text-red-500"; + return; + } + + const file = importXmlInput.files[0]; + + // Validate file type + if (!file.type.includes("xml")) { + importStatus.textContent = "Please select a valid XML file"; + importStatus.className = "text-xs mt-2 text-red-500"; + return; + } + + const formData = new FormData(); + formData.append("xmlfile", file); + + try { + importStatus.textContent = "Importing..."; + importStatus.className = "text-xs mt-2 text-blue-500"; + + const res = await fetch("/analytic/import", { + method: "POST", + body: formData, + }); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || "Import failed"); + } + + const data = await res.json(); + importStatus.textContent = data.message; + importStatus.className = "text-xs mt-2 text-green-500"; + + // Clear the file input + importXmlInput.value = ""; + + // Update the click counter to reflect new data + setTimeout(updateClickCounter, 1000); + } catch (err) { + importStatus.textContent = `Error: ${err.message}`; + importStatus.className = "text-xs mt-2 text-red-500"; + } + }); +} + // --- Reddit News Widget --- async function updateRedditNews() { try { diff --git a/public/stylesheets/output.css b/public/stylesheets/output.css index ca328ae..4e20aec 100644 --- a/public/stylesheets/output.css +++ b/public/stylesheets/output.css @@ -7,7 +7,12 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --color-red-100: oklch(93.6% 0.032 17.717); --color-red-500: oklch(63.7% 0.237 25.331); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-orange-600: oklch(64.6% 0.222 41.116); + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-400: oklch(79.2% 0.209 151.711); --color-green-500: oklch(72.3% 0.219 149.579); --color-green-600: oklch(62.7% 0.194 149.214); --color-sky-100: oklch(95.1% 0.026 236.824); @@ -15,19 +20,28 @@ --color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-700: oklch(48.8% 0.243 264.376); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-purple-600: oklch(55.8% 0.288 302.321); --color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-500: oklch(55.1% 0.027 264.364); --color-gray-600: oklch(44.6% 0.03 256.802); + --color-black: #000; --color-white: #fff; --spacing: 0.25rem; + --container-md: 28rem; --container-2xl: 42rem; + --container-4xl: 56rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25 / 1.875); --text-4xl: 2.25rem; @@ -189,15 +203,24 @@ .absolute { position: absolute; } + .fixed { + position: fixed; + } .relative { position: relative; } + .inset-0 { + inset: calc(var(--spacing) * 0); + } .top-\[calc\(100\%\+10px\)\] { top: calc(100% + 10px); } .z-10 { z-index: 10; } + .z-50 { + z-index: 50; + } .col-span-1 { grid-column: span 1 / span 1; } @@ -222,12 +245,18 @@ max-width: 96rem; } } + .mx-4 { + margin-inline: calc(var(--spacing) * 4); + } .mx-auto { margin-inline: auto; } .mt-2 { margin-top: calc(var(--spacing) * 2); } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } .mt-8 { margin-top: calc(var(--spacing) * 8); } @@ -255,6 +284,9 @@ .grid { display: grid; } + .hidden { + display: none; + } .table { display: table; } @@ -291,6 +323,12 @@ .max-w-2xl { max-width: var(--container-2xl); } + .max-w-4xl { + max-width: var(--container-4xl); + } + .max-w-md { + max-width: var(--container-md); + } .flex-1 { flex: 1; } @@ -303,6 +341,9 @@ .list-disc { list-style-type: disc; } + .grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } .grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); } @@ -321,6 +362,9 @@ .gap-2 { gap: calc(var(--spacing) * 2); } + .gap-4 { + gap: calc(var(--spacing) * 4); + } .gap-8 { gap: calc(var(--spacing) * 8); } @@ -331,6 +375,20 @@ margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-4 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-x-4 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); + } + } .overflow-y-auto { overflow-y: auto; } @@ -347,18 +405,39 @@ .border-gray-300 { border-color: var(--color-gray-300); } + .bg-black { + background-color: var(--color-black); + } .bg-blue-500 { background-color: var(--color-blue-500); } .bg-gray-100 { background-color: var(--color-gray-100); } + .bg-gray-500 { + background-color: var(--color-gray-500); + } + .bg-green-100 { + background-color: var(--color-green-100); + } + .bg-green-400 { + background-color: var(--color-green-400); + } .bg-green-500 { background-color: var(--color-green-500); } .bg-inherit { background-color: inherit; } + .bg-orange-500 { + background-color: var(--color-orange-500); + } + .bg-purple-500 { + background-color: var(--color-purple-500); + } + .bg-red-100 { + background-color: var(--color-red-100); + } .bg-sky-400 { background-color: var(--color-sky-400); } @@ -380,15 +459,24 @@ .p-8 { padding: calc(var(--spacing) * 8); } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } .px-3 { padding-inline: calc(var(--spacing) * 3); } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } .px-6 { padding-inline: calc(var(--spacing) * 6); } .px-8 { padding-inline: calc(var(--spacing) * 8); } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } .py-2 { padding-block: calc(var(--spacing) * 2); } @@ -398,6 +486,10 @@ .font-mono { font-family: var(--font-mono); } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } .text-3xl { font-size: var(--text-3xl); line-height: var(--tw-leading, var(--text-3xl--line-height)); @@ -414,6 +506,10 @@ font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } .text-xs { font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); @@ -430,6 +526,12 @@ --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); } + .break-all { + word-break: break-all; + } + .text-blue-500 { + color: var(--color-blue-500); + } .text-blue-700 { color: var(--color-blue-700); } @@ -439,6 +541,9 @@ .text-gray-600 { color: var(--color-gray-600); } + .text-green-500 { + color: var(--color-green-500); + } .text-red-500 { color: var(--color-red-500); } @@ -464,6 +569,13 @@ } } } + .hover\:bg-gray-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-600); + } + } + } .hover\:bg-green-600 { &:hover { @media (hover: hover) { @@ -471,6 +583,20 @@ } } } + .hover\:bg-orange-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-orange-600); + } + } + } + .hover\:bg-purple-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-purple-600); + } + } + } .hover\:bg-sky-100 { &:hover { @media (hover: hover) { @@ -502,12 +628,27 @@ outline-style: none; } } + .md\:grid-cols-2 { + @media (width >= 48rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .md\:grid-cols-4 { + @media (width >= 48rem) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } } @property --tw-space-y-reverse { syntax: "*"; inherits: false; initial-value: 0; } +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-border-style { syntax: "*"; inherits: false; @@ -586,6 +727,7 @@ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { --tw-space-y-reverse: 0; + --tw-space-x-reverse: 0; --tw-border-style: solid; --tw-font-weight: initial; --tw-shadow: 0 0 #0000; diff --git a/routes/index.js b/routes/index.js index dbbdd6a..72b1744 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,7 +7,9 @@ const airportData = JSON.parse( fs.readFileSync(path.join(__dirname, "../airportdata.json"), "utf8") ); const db = require("../models"); -const { create } = require("xmlbuilder2"); +const { create, parse } = require("xmlbuilder2"); +const speakeasy = require("speakeasy"); +const QRCode = require("qrcode"); const rateLimitMap = new Map(); const multer = require("multer"); const uploadDir = path.join(__dirname, "../public/uploads"); @@ -28,6 +30,15 @@ const redis = require("redis"); const client = redis.createClient(); client.connect().catch(console.error); +// 2FA middleware +function require2FA(req, res, next) { + if (req.session && req.session.twoFactorVerified) { + next(); + } else { + res.status(403).json({ error: "2FA verification required" }); + } +} + /* GET home page. */ router.get("/", function (req, res, next) { res.sendFile(path.join(__dirname, "../views/index.html")); @@ -147,6 +158,84 @@ router.get("/analytic/export", async function (req, res) { } }); +// Import analytic from XML +router.post( + "/analytic/import", + uploadMulter.single("xmlfile"), + async function (req, res) { + try { + if (!req.file) { + return res.status(400).json({ error: "No XML file uploaded" }); + } + + // Check file type + if ( + req.file.mimetype !== "text/xml" && + req.file.mimetype !== "application/xml" + ) { + return res.status(400).json({ error: "File must be XML format" }); + } + + // Read and parse the uploaded XML file + const xmlContent = fs.readFileSync(req.file.path, "utf8"); + const doc = parse(xmlContent); + + // Extract analytics data from XML + const analytics = []; + const analyticNodes = doc.find("//analytic"); + + for (const node of analyticNodes) { + const id = node.find("id")[0]?.text || null; + const createAt = node.find("create_at")[0]?.text || null; + const widgetName = node.find("widget_name")[0]?.text || null; + const browserType = node.find("browser_type")[0]?.text || null; + + if (widgetName && browserType) { + analytics.push({ + id: id ? parseInt(id) : undefined, + create_at: createAt ? new Date(createAt) : new Date(), + widget_name: widgetName, + browser_type: browserType, + }); + } + } + + if (analytics.length === 0) { + return res + .status(400) + .json({ error: "No valid analytics data found in XML" }); + } + + // Insert analytics into database + const insertedAnalytics = await db.analytic.bulkCreate(analytics, { + ignoreDuplicates: true, + updateOnDuplicate: ["widget_name", "browser_type"], + }); + + // Clean up uploaded file + fs.unlinkSync(req.file.path); + + res.json({ + success: true, + message: `Successfully imported ${insertedAnalytics.length} analytics records`, + count: insertedAnalytics.length, + }); + } catch (err) { + // Clean up uploaded file on error + if (req.file && req.file.path) { + try { + fs.unlinkSync(req.file.path); + } catch (cleanupErr) { + console.error("Failed to cleanup uploaded file:", cleanupErr); + } + } + + console.error("Import error:", err); + res.status(500).json({ error: "Failed to import analytics from XML" }); + } + } +); + // Reddit widget route router.get("/reddit", async function (req, res) { try { @@ -484,4 +573,82 @@ router.get("/flow/:id/trigger", async function (req, res) { } }); +// 2FA Routes +// Generate 2FA secret +router.get("/2fa/generate", async function (req, res) { + try { + const secret = speakeasy.generateSecret({ + name: "Dashboard 2FA", + length: 20, + }); + + // Store secret in session for verification + req.session = req.session || {}; + req.session.tempSecret = secret.base32; + + // Generate QR code as data URL + const qrCodeDataUrl = await QRCode.toDataURL(secret.otpauth_url, { + width: 200, + margin: 2, + color: { + dark: "#000000", + light: "#FFFFFF", + }, + }); + + res.json({ + secret: secret.base32, + qrCode: qrCodeDataUrl, + qrCodeUrl: secret.otpauth_url, + }); + } catch (err) { + console.error("2FA generation error:", err); + res.status(500).json({ error: "Failed to generate 2FA secret" }); + } +}); + +// Verify 2FA token +router.post("/2fa/verify", function (req, res) { + try { + const { token } = req.body; + const tempSecret = req.session?.tempSecret; + + if (!tempSecret) { + return res + .status(400) + .json({ error: "No 2FA secret found. Please generate a new one." }); + } + + if (!token) { + return res.status(400).json({ error: "Token required" }); + } + + const verified = speakeasy.totp.verify({ + secret: tempSecret, + encoding: "base32", + token: token, + window: 2, // Allow 2 time steps for clock skew + }); + + if (verified) { + // Mark 2FA as verified in session + req.session = req.session || {}; + req.session.twoFactorVerified = true; + delete req.session.tempSecret; // Clean up temp secret + + res.json({ success: true, message: "2FA verification successful" }); + } else { + res.status(400).json({ error: "Invalid 2FA token" }); + } + } catch (err) { + res.status(500).json({ error: "Failed to verify 2FA token" }); + } +}); + +// Check 2FA status +router.get("/2fa/status", function (req, res) { + const verified = req.session?.twoFactorVerified || false; + res.json({ verified }); +}); + module.exports = router; diff --git a/views/index.html b/views/index.html index f5446b2..895e6b1 100644 --- a/views/index.html +++ b/views/index.html @@ -80,6 +80,25 @@ https://cdn.jsdelivr.net/npm/tailwindcss@4.1.11/dist/lib.min.js Export + +
+
Import XML
+ + +
+
+ + + +