diff --git a/.env.example b/.env.example index d3d72e1..255daf9 100755 --- a/.env.example +++ b/.env.example @@ -1,2 +1,7 @@ -mongoURI='mongodb://localhost:27017/' -database='nameOfDatabase' +MONGO_URI='mongodb://localhost:27017/' +DATABASE='nameOfDatabase' +PORT=8000 +SECRET='123456789' +GEOLOCATION_API='api_key' +EMAIL_USER=mail@example.com +EMAIL_PASS='password' diff --git a/.gitignore b/.gitignore index 7e55289..7734288 100755 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ # Environment variables .env +#vscode files +.vscode + # Node modules node_modules/ # Sync *.sync-conflict* + +# VSCode +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 766c1ce..aa462a5 100755 --- a/README.md +++ b/README.md @@ -24,11 +24,27 @@ Example: ## Usage -***Please check back later.*** +1. **Clone the repository:** + ```bash + git clone https://github.com/JoaquinPar/2800-202510-BBY14.git + cd 2800-202510-BBY14 + ``` -1. ... -2. ... -3. ... +2. **Install dependencies:** + ```bash + npm install + ``` + This will install Express, express-session, EJS, dotenv, and any other required packages listed in `package.json`. + +3. **Create and configure the environment file:** + - Create a file named `.env` in the root directory of the project. + - Add the necessary environment variables to this file. + +4. **Run the application:** + ```bash + node app.js + ``` + The application should now be running, typically on `http://localhost:3000` (or the port specified in your `.env` file). --- @@ -37,14 +53,14 @@ Example: ``` retirementCalculator/ ├── src/ -| ├── app.js │ ├── views/ -│ ├── public/ -| | ├── css/ -| | ├── icons/ -| | ├── images/ -| | └── scripts/ -│ └── util/ +│ │ └── partials/ +│ ├── css/ +│ ├── images/ +│ ├── scripts/ +│ └── utils/ +│ +├── app.js ├── .env.example ├── .gitignore ├── LICENSE diff --git a/about.html b/about.html deleted file mode 100644 index 7c27b52..0000000 --- a/about.html +++ /dev/null @@ -1,12 +0,0 @@ - - - Team Name: BBY-14 - Team Members: - - - diff --git a/app.js b/app.js new file mode 100644 index 0000000..56eb793 --- /dev/null +++ b/app.js @@ -0,0 +1,140 @@ +const status = require("./src/util/statuses"); +const MongoStore = require("connect-mongo"); +const session = require("express-session"); +const express = require('express'); +const path = require('path'); +const bcrypt = require('bcrypt'); +const joi = require('joi'); +require('dotenv').config(); + +const app = express(); +const port = process.env.PORT || 3000; + +const mongoURI = process.env.MONGO_URI; +const database = process.env.DATABASE; // Database name +const secret = process.env.SECRET || "123-secret-xyz"; + +/*** Sessions ***/ +app.use(session({ + secret: secret, + store: MongoStore.create({ mongoUrl: `${mongoURI}${database}`, crypto: { secret: secret } }), + resave: true, + saveUninitialized: false, + cookie: { maxAge: 3600000 }, +})); + +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'src/views')); +app.use(express.urlencoded({ extended: true })); +app.use("/static", express.static("./src/public")); +app.use("/images", express.static("./src/public/images")); +app.use(express.json()); + +/*** Database ***/ +const { connectMongo, getCollection } = require("./src/database/connection"); + +let users; +let assets; +let plans; +async function initDatabase() { + const db = await connectMongo(mongoURI, database); + + // For any collection, init here + users = await getCollection(db, "users"); + assets = await getCollection(db, "assets"); + plans = await getCollection(db, "plans"); +} + +/*** ROUTINGS ***/ + +app.get('/', (req, res) => { + if (!req.session.errMessage) req.session.errMessage = ""; + res.render('landing'); + return res.status(status.Ok); +}); + +app.get('/signup', (req, res) => { + const ignore = ["User not found", "Incorrect password"]; + if (ignore.includes(req.session.errMessage)) req.session.errMessage = ""; + res.render('signup', { errMessage: req.session.errMessage }); + return res.status(status.Ok); +}); + +app.get('/login', (req, res) => { + if (req.session.authenticated) { + res.redirect("/home"); + return res.status(status.Ok); + } + res.render('login', { errMessage: req.session.errMessage }); + return res.status(status.Ok); +}); + +app.get('/aboutUs', (req, res) => { + res.render('aboutUs'); + return res.status(status.Ok); +}); + +app.get('/forgotPassword', (req, res) => { + const error = req.session.error; + const reset = req.session.reset; + delete req.session.reset; + delete req.session.error; + res.render('forgotPass', { error: error, reset: reset }); + return res.status(status.Ok); +}); + + +// Reset with token given to user via email +app.get('/reset/:token', async (req, res) => { + const token = req.params.token; + + const user = await users.findOne({ + resetToken: token, + resetTokenExpires: { $gt: Date.now() }, + }); + + if (!user) { + req.session.error = 'reset link not valid or has expired'; + return res.redirect('/forgotPassword'); + } + const error = req.session.error; + delete req.session.error; + + res.render('resetPass', { + token: token, + errMessage: error, + }); +}); + + +// 404 handler - keep the actual notFound route please +// REALLY DONT DELETE THIS +app.get('/notFound', (req, res) => { + res.render('notFound'); + return res.status(status.NotFound); +}); + +// Initialize database and start app +initDatabase().then(() => { + console.log("Successfully connected to MongoDB"); + + // Import authentication handler + app.use(require("./src/auth/authentication")(users)); + app.use(require('./src/auth/forgotPass')(users)); + + + // Import middleware & apply to user routes + const middleware = require("./src/auth/middleware")(users); + app.use(require('./src/router/user')(middleware, users, plans, assets)); + + // 404 handler + app.get('/*splat', (req, res) => { + res.render('notFound'); + return res.status(status.NotFound); + }); + + // Start app + app.listen(port, () => { + console.log(`Server listening on port ${port}`); + }); +}); diff --git a/package-lock.json b/package-lock.json old mode 100755 new mode 100644 index ba46134..83cff57 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,107 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "express": "^5.1.0" + "bcrypt": "^5.1.1", + "connect-mongo": "^5.1.0", + "crypto": "^1.0.1", + "dotenv": "^16.5.0", + "ejs": "^3.1.10", + "express": "^5.1.0", + "express-session": "^1.18.1", + "joi": "^17.13.3", + "mongodb": "^6.16.0", + "nodemailer": "^7.0.3" }, "devDependencies": { "nodemon": "^3.1.10" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -28,6 +123,42 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -42,13 +173,64 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -62,6 +244,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -86,7 +274,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -106,6 +293,15 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -144,6 +340,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -169,13 +381,71 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, + "node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -215,6 +485,12 @@ "node": ">=6.6.0" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -232,6 +508,12 @@ } } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -241,6 +523,27 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -261,6 +564,27 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -357,6 +681,76 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "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-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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -405,6 +799,36 @@ "node": ">= 0.8" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -429,6 +853,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -466,6 +911,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -492,13 +958,12 @@ } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-symbols": { @@ -513,6 +978,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -541,6 +1012,19 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -560,6 +1044,17 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -598,6 +1093,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", @@ -627,6 +1131,73 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/kruptein": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.7.tgz", + "integrity": "sha512-vTftnEjfbqFHLqxDUMQCj6gBo5lKqjV4f0JsM8rk8rM3xmvFZ2eSy4YALdaye7E+cDKnEj7eAjFR3vwh8a4PgQ==", + "license": "MIT", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -645,6 +1216,12 @@ "node": ">= 0.8" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -678,11 +1255,16 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -691,6 +1273,108 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mongodb": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", + "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.3", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -706,6 +1390,63 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/nodemailer": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -735,6 +1476,44 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -745,6 +1524,28 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -769,6 +1570,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -787,6 +1597,15 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -829,6 +1648,15 @@ "dev": true, "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -844,6 +1672,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", @@ -868,6 +1705,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -881,6 +1732,22 @@ "node": ">=8.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -927,7 +1794,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -973,6 +1839,12 @@ "node": ">= 18" } }, + "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.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1051,6 +1923,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1064,6 +1942,15 @@ "node": ">=10" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1073,17 +1960,68 @@ "node": ">= 0.8" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "safe-buffer": "~5.2.0" + } + }, + "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": ">=4" + "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/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/to-regex-range": { @@ -1118,6 +2056,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1132,6 +2082,18 @@ "node": ">= 0.6" } }, + "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/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1148,6 +2110,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1157,11 +2125,48 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "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==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 69f119c..1071779 100755 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "retirementcalculator", "version": "0.1.0", "description": "Retirement Calculator", - "main": "./src/app.js", + "main": "app.js", "scripts": { - "start": "node ./src/app.js", - "dev": "nodemon ./src/app.js" + "start": "node app.js", + "dev": "nodemon app.js" }, "repository": { "type": "git", @@ -18,7 +18,16 @@ }, "homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme", "dependencies": { - "express": "^5.1.0" + "bcrypt": "^5.1.1", + "connect-mongo": "^5.1.0", + "crypto": "^1.0.1", + "dotenv": "^16.5.0", + "ejs": "^3.1.10", + "express": "^5.1.0", + "express-session": "^1.18.1", + "joi": "^17.13.3", + "mongodb": "^6.16.0", + "nodemailer": "^7.0.3" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 71e5912..0000000 --- a/src/app.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require("express"); -const path = require("path"); - -const app = express(); -const port = 8000; - -app.listen(port, () => { - console.log(`Server listening on port ${port}`); -}); \ No newline at end of file diff --git a/src/auth/authentication.js b/src/auth/authentication.js new file mode 100644 index 0000000..d65e9c6 --- /dev/null +++ b/src/auth/authentication.js @@ -0,0 +1,109 @@ +const status = require("../util/statuses"); +const session = require("express-session"); +const bcrypt = require('bcrypt'); +const joi = require("joi"); +const salt = 12; + +/** + * @param {MongoClient.collection} users db collection + * @returns {express.Router} authentication router + */ +module.exports = (users) => { + const router = require("express").Router(); + + router.get("/logout", (req, res) => { + req.session.destroy(); + return res.redirect('/login'); + }); + + router.post("/login", async (req, res) => { + + const credentialSchema = joi.object({ + email: joi.string().email().required(), + password: joi.string().alphanum().max(20).required(), + }); + + const valid = credentialSchema.validate(req.body); + + if (valid.err) { + req.session.errMessage = "Invalid input"; + res.status(status.BadRequest); + return res.redirect("/login"); + } + + users.findOne({ "email": req.body.email }).then((user) => { + if (!user) { + req.session.errMessage = "User not found"; + res.status(status.NotFound); + return res.redirect("/login"); + } + + if (!bcrypt.compareSync(req.body.password, user.password)) { + req.session.errMessage = "Incorrect password"; + res.status(status.Unauthorized); + return res.redirect("/login"); + } + + req.session.authenticated = true; + req.session.userId = user._id; + req.session.email = req.body.email; + req.session.errMessage = ""; + res.redirect("/home"); + return res.status(status.Ok); + }); + }); + + router.post("/signup", async (req, res) => { + const userSchema = joi.object({ + email: joi.string().email().required(), + name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20).required(), + password: joi.string().alphanum().max(20).min(8).required(), + repassword: joi.string().alphanum().max(20).min(8).required(), + }); + + const valid = userSchema.validate(req.body); + + if (valid.err) { + req.session.errMessage = "Invalid input", + res.status(status.BadRequest); + return res.redirect("/signup"); + } + + let exists = await users.findOne({ email: req.body.email }).then((exists) => exists); + if (exists) { + req.session.errMessage = "Email already in use"; + res.status(status.BadRequest); + return res.redirect("/signup"); + } + + if (req.body.password != req.body.repassword) { + req.session.errMessage = "Passwords must match"; + res.status(status.BadRequest); + return res.redirect("/signup"); + } + + let hashedPassword = await bcrypt.hashSync(req.body.password, salt); + + users.insertOne({ + email: req.body.email, + name: req.body.name, + password: hashedPassword, + financialData: false, + }).then((results, err) => { + if (err) { + console.error("Error creating user on signup: ", err); + res.session.errMessage = "Internal server error"; + return res.status(status.InternalServerError).redirect("/signup"); + } + + req.session.authenticated = true; + req.session.email = req.body.email; + req.session.userId = results.insertedId; + + req.session.errMessage = ""; + return res.status(status.Ok).redirect("/home"); + }); + }); + + return router; +} diff --git a/src/auth/forgotPass.js b/src/auth/forgotPass.js new file mode 100644 index 0000000..698a500 --- /dev/null +++ b/src/auth/forgotPass.js @@ -0,0 +1,118 @@ +const express = require('express'); +const crypto = require('crypto'); +const joi = require('joi'); +const nodeMail = require('nodemailer'); +const bcrypt = require('bcrypt'); +require('dotenv').config(); +const PORT = process.env.PORT; + +const transporter = nodeMail.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + } +}); +// users info +module.exports = (users) => { + const router = express.Router(); + + router.post('/auth/resetPass', async (req, res) => { + const resetSchema = joi.object({ + email: joi.string().email().required(), + }); + req.session.error = ''; + req.session.reset = ''; + + const valid = resetSchema.validate(req.body); + if (valid.error) { + req.session.error = 'invalid email'; + return res.redirect('/forgotPassword') + } + const { email } = req.body; + const user = await users.findOne({ email }); + + if (!user) { + req.session.error = 'No user found' + return res.redirect('/forgotPassword'); + } + + const token = crypto.randomBytes(32).toString('hex'); + console.log(`The reset token is ${token}`) + const expiration = Date.now() + 360000; + + await users.updateOne({ email }, { + $set: { resetToken: token, resetTokenExpires: expiration } + }); + + const resetUrl = `http://localhost:${PORT}/reset/${token}`; + + const mailSend = { + from: process.env.EMAIL_USER, + to: email, + subject: 'Password reset', + text: `reset your password here ${resetUrl} this link will expire within 1 hour`, + + }; + + try { + await transporter.sendMail(mailSend); + req.session.reset = 'Reset link sent Check your email'; + res.redirect('/forgotPassword'); + } catch (err) { + console.log('there was an error', err); + res.status(500).send('email failed to send try again'); + } + }); + + router.post('/resetLink', async (req, res) => { + const { token, password, confirmPassword, } = req.body; + const passwordSchema = joi.object({ + password: joi.string().max(20).required(), + confirmPassword: joi.string().max(20).required(), + }); + const valid = passwordSchema.validate({ password, confirmPassword }); + if (valid.error) { + console.log("houston we have a problem"); + req.session.error = 'Invalid input'; + res.status(status.BadRequest); + return res.redirect(`/reset/${token}`); + } + if (!token || !password || !confirmPassword) { + req.session.error = 'field may be missing'; + return res.redirect(`/reset/${token}`); + } + if (password !== confirmPassword) { + req.session.error = 'passwords do not match'; + return res.redirect(`/reset/${token}`); + } + const user = await users.findOne({ + resetToken: token, + resetTokenExpires: { $gt: Date.now() }, + }); + + if (!user) { + req.session.error = 'Reset link is invalid.'; + return res.redirect(`/reset`); + } + const hashPassword = await bcrypt.hash(password, 12); + + await users.updateOne( + { + email: user.email + }, + { + $set: { + password: hashPassword, + resetToken: '', + resetTokenExpires: 0, + }, + } + ); + req.session.success = 'Password has been reset'; + res.redirect('/login'); + }); + + return router; +} + diff --git a/src/auth/middleware.js b/src/auth/middleware.js new file mode 100644 index 0000000..5c24807 --- /dev/null +++ b/src/auth/middleware.js @@ -0,0 +1,43 @@ +const status = require("../util/statuses"); +const session = require("express-session"); + +// Get all names of user routes +let userRouter = require("../router/user")((req, res, next) => next(), null, null, null); +userRouter.stack.shift(); +const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1]); + +/** + * @param {MongoClient.collection} users db collection + * @returns {async function} middleware handler function +*/ +const createMiddleware = (users) => { + return async (req, res, next) => { + // Check if incoming request route exists in user routes + // redirect to 404 if not + if (!userRoutes.includes(req.url.split("/")[1].split("?")[0])) { + return res.status(status.NotFound).redirect("/notFound"); + } + + if (!req.session.authenticated || !req.session.email) { + req.session.errMessage = "Please login to view that resource"; + res.redirect("/login"); + return res.status(status.Unauthorized); + } + + if (!req.session.user) { + let user = await users.findOne({ "email": req.session.email }).then((user) => user); + + if (!user) { + req.session.errMessage = "User not found"; + res.redirect("/login"); + return res.status(status.Unauthorized); + } + + req.session.user = user; + } + + next(); + }; +} + +module.exports = createMiddleware; diff --git a/src/database/connection.js b/src/database/connection.js new file mode 100644 index 0000000..18ac54f --- /dev/null +++ b/src/database/connection.js @@ -0,0 +1,28 @@ +const MongoClient = require("mongodb").MongoClient; + +/** + * connectMongo returns a database connection to MongoDB + * @param {string} mongoURI + * @param {string} databaseName + * @return {MongoClient} + */ +const connectMongo = async (mongoURI, databaseName) => { + const database = await MongoClient.connect(mongoURI, { connectTimeoutMS: 1000 }); + const dbo = database.db(databaseName); + return dbo; +} + +/** + * getCollection object to interact with MongoDB + * @param {MongoClient} dbo + * @param {string} collection + * @return {MongoClient.collection} + */ +const getCollection = async (dbo, collection) => { + return await dbo.collection(collection); +} + +module.exports = { + connectMongo: connectMongo, + getCollection: getCollection, +} diff --git a/src/public/images/bg1.jpg b/src/public/images/bg1.jpg new file mode 100644 index 0000000..c39e9e1 Binary files /dev/null and b/src/public/images/bg1.jpg differ diff --git a/src/public/scripts/assetManager.js b/src/public/scripts/assetManager.js new file mode 100644 index 0000000..16bbc37 --- /dev/null +++ b/src/public/scripts/assetManager.js @@ -0,0 +1,148 @@ +const assetForms = [ + "create-other-asset-form", + "create-saving-asset-form", + "create-stock-asset-form" +]; + +/** + * resetAll asset creation forms + */ +function resetAll() { + document.getElementById("dropdown-icon-button").innerHTML = ` + Other Other + + `; + document.getElementById("create-other-asset-form").reset(); + document.getElementById("create-saving-asset-form").reset(); + document.getElementById("create-stock-asset-form").reset(); +} + +/** + * selectAssetForm changes which form is displayed + * to create specified assets. + * @param {string} assetFormId + */ +function selectAssetForm(assetFormId) { + for (let i = 0; i < assetForms.length; i++) { + if (assetForms[i] == assetFormId) { + document.getElementById(assetFormId).style.display = "block"; + } else { + document.getElementById(assetForms[i]).style.display = "none"; + } + } +} + +/** + * resetRadio to default to other asset type. + */ +function resetRadio() { + document.getElementById("asset-other").checked = true; + document.getElementById("asset-saving").checked = false; + document.getElementById("asset-stock").checked = false; +} + +const assetKeys = { + other: [ + "name", + "dropdown-icon-button", + "value", + "description", + "purchaseDate", + ], + saving: [ + "name", + "value", + ], + stock: [ + "ticker", + "price", + "quantity", + "purchaseDate", + ], +} + +/** + * lockAsset prevents edits to asset view modal + * @param {string} assetId + * @param {string} icon to defualt to + */ +function lockAsset(assetId, icon) { + let dropdown = document.getElementById(`dropdown-icon-button-${assetId}`); + if (dropdown) dropdown.innerHTML = ` + ${icon} ${icon} + + `; + + document.getElementById(`${assetId}-form`).reset(); + + const type = document.getElementById(`type-${assetId}`).value; + + assetKeys[type].forEach((key) => { + document.getElementById(`${key}-${assetId}`).disabled = true; + }); + + document.getElementById(`save-${assetId}`).disabled = true; + document.getElementById(`save-${assetId}`).classList.remove("cursor-pointer"); + document.getElementById(`save-${assetId}`).classList.add("cursor-not-allowed"); + + document.getElementById(`edit-${assetId}`).innerHTML = "Edit"; + document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId, icon) }; +} + +/** + * unlockAsset allows edits to asset view modal + * @param {string} assetId + * @param {string} icon to defualt to + */ +function unlockAsset(assetId, icon) { + const type = document.getElementById(`type-${assetId}`).value; + + assetKeys[type].forEach((key) => { + document.getElementById(`${key}-${assetId}`).disabled = false; + }); + + document.getElementById(`save-${assetId}`).disabled = false; + document.getElementById(`save-${assetId}`).classList.remove("cursor-not-allowed"); + document.getElementById(`save-${assetId}`).classList.add("cursor-pointer"); + + document.getElementById(`edit-${assetId}`).innerHTML = "Cancel"; + document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId, icon) }; +} + +/** + * autoOpenCreate checks if popup param + * in url to auto open create popup + */ +function autoOpenCreate() { + const query = window.location.search; + const params = new URLSearchParams(query); + + if (params.has("popup")) { + document.getElementById('create-asset-modal').showModal(); + } +} + +/** + * selectIcon updated selected icon while creating + * or modifying assets. + * @param {button element} selectedIcon + * @param {string} assetId + */ +function selectIcon(selectedIcon, assetId="") { + document.getElementById(`dropdown-icon-button${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value; + document.getElementById(`icon${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value + + document.getElementById(`dropdown-icon-button${assetId != "" ? "-" : ""}${assetId}`).innerHTML = selectedIcon.innerHTML + + ` + + `; +} + +resetRadio(); +autoOpenCreate(); diff --git a/src/public/scripts/calcExchange.js b/src/public/scripts/calcExchange.js new file mode 100644 index 0000000..29956e8 --- /dev/null +++ b/src/public/scripts/calcExchange.js @@ -0,0 +1,3 @@ +let rate = document.getElementsByClassName("countryButton").value; + +document.getElementById("exchange").innerHTML \ No newline at end of file diff --git a/src/public/scripts/geolocation.js b/src/public/scripts/geolocation.js new file mode 100644 index 0000000..7fce450 --- /dev/null +++ b/src/public/scripts/geolocation.js @@ -0,0 +1,69 @@ +function getLocation() { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(getLatestExchange, error); + } +} + +function update(data) { + document.getElementById("loading").style = "display: none"; + document.getElementById("currencyExchange").style = "display: flex"; + + document.getElementById("yourFlag").src = `/static/svgs/flags/${data.data.country}.svg` + document.getElementById("yourFlag").alt = data.data.country; + document.getElementById("flagTag").innerHTML = `(${data.data.country})`; + + document.getElementById("exchangeFlag").src = `/static/svgs/flags/USD.svg`; + document.getElementById("exchangeFlag").alt = "USD"; + document.getElementById("exFlagTag").innerHTML = "(USD)"; + + document.getElementById("dropdown-country-button").value = data.data.toCurrencyRates["USD"]; + + updateExchange(document.getElementById("dropdown-country-button").value); + + const dropdown = document.getElementById("dropdown"); + let countries = Object.keys(data.data.toCurrencyRates); + + countries.forEach(item => { + if (dropdown.querySelector(`button[value="${data.data.toCurrencyRates[item]}"]`)) { + return; + } + + const listItem = document.createElement("li"); + listItem.innerHTML = ``; + + dropdown.appendChild(listItem); + }); +} + +function switchButton(clickedButton) { + document.getElementById("dropdown-country-button").value = clickedButton.value; + updateExchange(document.getElementById("dropdown-country-button").value); + + document.getElementById("dropdown-country-button").innerHTML = clickedButton.innerHTML + + ` + + `; +} + +function updateExchange(exRate) { + document.getElementById("exchange").innerHTML = "$1.00 = $" +`${(1 * exRate).toFixed(2)}`; +} + +async function getLatestExchange(position) { + let lat = position.coords.latitude; + let lon = position.coords.longitude; + const res = await fetch(`/exRates/${lat}/${lon}`); + const data = await res.json(); + + update(data); +} + +function error(err) { + console.error("Geolocation error: ", err); +} \ No newline at end of file diff --git a/src/public/scripts/profile.js b/src/public/scripts/profile.js new file mode 100644 index 0000000..d803378 --- /dev/null +++ b/src/public/scripts/profile.js @@ -0,0 +1,75 @@ +/** + * lockAccount resets inputs and disabled inputs + */ +function lockAccount() { + // Clear unsaved inputs on page load (refresh doesnt clear them) + document.getElementById("account-form").reset(); + + document.getElementById("save-account").disabled = true; + document.getElementById("email").disabled = true; + document.getElementById("name").disabled = true; + document.getElementById("password").disabled = true; + document.getElementById("repassword").disabled = true; + document.getElementById("save-account").classList.add("cursor-not-allowed"); + + document.getElementById("edit-account").innerHTML = "Edit"; + document.getElementById("edit-account").onclick = unlockAccount; +} + +/** + * lockPersonal resets inputs and disabled inputs + */ +function lockPersonal() { + // Clear unsaved inputs on page load (refresh doesnt clear them) + document.getElementById("personal-form").reset(); + + document.getElementById("save-personal").disabled = true; + document.getElementById("dob").disabled = true; + document.getElementById("education").disabled = true; + document.getElementById("ms-single").disabled = true; + document.getElementById("ms-married").disabled = true; + document.getElementById("ms-divorced").disabled = true; + document.getElementById("ms-widowed").disabled = true; + document.getElementById("save-personal").classList.add("cursor-not-allowed"); + + document.getElementById("edit-personal").innerHTML = "Edit"; + document.getElementById("edit-personal").onclick = unlockPersonal; +} + +/** + * unlockAccount removes disabled from inputs and + * allows users to edit their profile. + */ +function unlockAccount() { + document.getElementById("save-account").disabled = false; + // document.getElementById("email").disabled = false; + document.getElementById("name").disabled = false; + document.getElementById("password").disabled = false; + document.getElementById("repassword").disabled = false; + document.getElementById("save-account").classList.remove("cursor-not-allowed"); + + document.getElementById("edit-account").innerHTML = "Cancel changes"; + document.getElementById("edit-account").onclick = lockAccount; +} + +/** + * unlocPersonal removes disabled from inputs and + * allows users to edit their personal information. + */ +function unlockPersonal() { + document.getElementById("save-personal").disabled = false; + document.getElementById("dob").disabled = false; + document.getElementById("education").disabled = false; + document.getElementById("ms-single").disabled = false; + document.getElementById("ms-married").disabled = false; + document.getElementById("ms-divorced").disabled = false; + document.getElementById("ms-widowed").disabled = false; + document.getElementById("save-personal").classList.remove("cursor-not-allowed"); + + document.getElementById("edit-personal").innerHTML = "Cancel changes"; + document.getElementById("edit-personal").onclick = lockPersonal; +} + +// On page load, ensure forms are locked and reset +lockAccount(); +// lockPersonal(); diff --git a/src/public/svgs/assets.svg b/src/public/svgs/assets.svg new file mode 100644 index 0000000..824466d --- /dev/null +++ b/src/public/svgs/assets.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/src/public/svgs/dashboard.svg b/src/public/svgs/dashboard.svg new file mode 100644 index 0000000..e10e231 --- /dev/null +++ b/src/public/svgs/dashboard.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/public/svgs/flags/AUD.svg b/src/public/svgs/flags/AUD.svg new file mode 100644 index 0000000..96e8076 --- /dev/null +++ b/src/public/svgs/flags/AUD.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/public/svgs/flags/BGN.svg b/src/public/svgs/flags/BGN.svg new file mode 100644 index 0000000..af2d0d0 --- /dev/null +++ b/src/public/svgs/flags/BGN.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/public/svgs/flags/BRL.svg b/src/public/svgs/flags/BRL.svg new file mode 100644 index 0000000..fe1d416 --- /dev/null +++ b/src/public/svgs/flags/BRL.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/CAD.svg b/src/public/svgs/flags/CAD.svg new file mode 100644 index 0000000..c9b23b4 --- /dev/null +++ b/src/public/svgs/flags/CAD.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/svgs/flags/CHF.svg b/src/public/svgs/flags/CHF.svg new file mode 100644 index 0000000..b42d670 --- /dev/null +++ b/src/public/svgs/flags/CHF.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/public/svgs/flags/CNY.svg b/src/public/svgs/flags/CNY.svg new file mode 100644 index 0000000..10d3489 --- /dev/null +++ b/src/public/svgs/flags/CNY.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/public/svgs/flags/CZK.svg b/src/public/svgs/flags/CZK.svg new file mode 100644 index 0000000..7913de3 --- /dev/null +++ b/src/public/svgs/flags/CZK.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/public/svgs/flags/DKK.svg b/src/public/svgs/flags/DKK.svg new file mode 100644 index 0000000..563277f --- /dev/null +++ b/src/public/svgs/flags/DKK.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/public/svgs/flags/EUR.svg b/src/public/svgs/flags/EUR.svg new file mode 100644 index 0000000..b0874c1 --- /dev/null +++ b/src/public/svgs/flags/EUR.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/GBP.svg b/src/public/svgs/flags/GBP.svg new file mode 100644 index 0000000..7991383 --- /dev/null +++ b/src/public/svgs/flags/GBP.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/public/svgs/flags/HKD.svg b/src/public/svgs/flags/HKD.svg new file mode 100644 index 0000000..4fd55bc --- /dev/null +++ b/src/public/svgs/flags/HKD.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/public/svgs/flags/HUF.svg b/src/public/svgs/flags/HUF.svg new file mode 100644 index 0000000..baddf7f --- /dev/null +++ b/src/public/svgs/flags/HUF.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/public/svgs/flags/IDR.svg b/src/public/svgs/flags/IDR.svg new file mode 100644 index 0000000..3b7c8fc --- /dev/null +++ b/src/public/svgs/flags/IDR.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/svgs/flags/ILS.svg b/src/public/svgs/flags/ILS.svg new file mode 100644 index 0000000..f43be7e --- /dev/null +++ b/src/public/svgs/flags/ILS.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/INR.svg b/src/public/svgs/flags/INR.svg new file mode 100644 index 0000000..bc47d74 --- /dev/null +++ b/src/public/svgs/flags/INR.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/ISK.svg b/src/public/svgs/flags/ISK.svg new file mode 100644 index 0000000..a6588af --- /dev/null +++ b/src/public/svgs/flags/ISK.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/public/svgs/flags/JPY.svg b/src/public/svgs/flags/JPY.svg new file mode 100644 index 0000000..cc1c181 --- /dev/null +++ b/src/public/svgs/flags/JPY.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/public/svgs/flags/KRW.svg b/src/public/svgs/flags/KRW.svg new file mode 100644 index 0000000..6947eab --- /dev/null +++ b/src/public/svgs/flags/KRW.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/MXN.svg b/src/public/svgs/flags/MXN.svg new file mode 100644 index 0000000..5a67d62 --- /dev/null +++ b/src/public/svgs/flags/MXN.svg @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/MYR.svg b/src/public/svgs/flags/MYR.svg new file mode 100644 index 0000000..115f864 --- /dev/null +++ b/src/public/svgs/flags/MYR.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/NOK.svg b/src/public/svgs/flags/NOK.svg new file mode 100644 index 0000000..a5f2a15 --- /dev/null +++ b/src/public/svgs/flags/NOK.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/public/svgs/flags/NZD.svg b/src/public/svgs/flags/NZD.svg new file mode 100644 index 0000000..935d8a7 --- /dev/null +++ b/src/public/svgs/flags/NZD.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/PHP.svg b/src/public/svgs/flags/PHP.svg new file mode 100644 index 0000000..b910e24 --- /dev/null +++ b/src/public/svgs/flags/PHP.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/public/svgs/flags/PLN.svg b/src/public/svgs/flags/PLN.svg new file mode 100644 index 0000000..0fa5145 --- /dev/null +++ b/src/public/svgs/flags/PLN.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/public/svgs/flags/RON.svg b/src/public/svgs/flags/RON.svg new file mode 100644 index 0000000..fda0f7b --- /dev/null +++ b/src/public/svgs/flags/RON.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/public/svgs/flags/RUB.svg b/src/public/svgs/flags/RUB.svg new file mode 100644 index 0000000..cf24301 --- /dev/null +++ b/src/public/svgs/flags/RUB.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/public/svgs/flags/SEK.svg b/src/public/svgs/flags/SEK.svg new file mode 100644 index 0000000..8ba745a --- /dev/null +++ b/src/public/svgs/flags/SEK.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/svgs/flags/SGD.svg b/src/public/svgs/flags/SGD.svg new file mode 100644 index 0000000..c4dd4ac --- /dev/null +++ b/src/public/svgs/flags/SGD.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/public/svgs/flags/THB.svg b/src/public/svgs/flags/THB.svg new file mode 100644 index 0000000..1e93a61 --- /dev/null +++ b/src/public/svgs/flags/THB.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/public/svgs/flags/TRY.svg b/src/public/svgs/flags/TRY.svg new file mode 100644 index 0000000..b96da21 --- /dev/null +++ b/src/public/svgs/flags/TRY.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/public/svgs/flags/USD.svg b/src/public/svgs/flags/USD.svg new file mode 100644 index 0000000..9cfd0c9 --- /dev/null +++ b/src/public/svgs/flags/USD.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/public/svgs/flags/ZAR.svg b/src/public/svgs/flags/ZAR.svg new file mode 100644 index 0000000..d563adb --- /dev/null +++ b/src/public/svgs/flags/ZAR.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/public/svgs/icons/Bike.svg b/src/public/svgs/icons/Bike.svg new file mode 100644 index 0000000..c31ed29 --- /dev/null +++ b/src/public/svgs/icons/Bike.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Book.svg b/src/public/svgs/icons/Book.svg new file mode 100644 index 0000000..d1cd59b --- /dev/null +++ b/src/public/svgs/icons/Book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Camera.svg b/src/public/svgs/icons/Camera.svg new file mode 100644 index 0000000..4fda7d9 --- /dev/null +++ b/src/public/svgs/icons/Camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Car.svg b/src/public/svgs/icons/Car.svg new file mode 100644 index 0000000..63014de --- /dev/null +++ b/src/public/svgs/icons/Car.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Coins.svg b/src/public/svgs/icons/Coins.svg new file mode 100644 index 0000000..17fee69 --- /dev/null +++ b/src/public/svgs/icons/Coins.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Console.svg b/src/public/svgs/icons/Console.svg new file mode 100644 index 0000000..932964d --- /dev/null +++ b/src/public/svgs/icons/Console.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Desktop.svg b/src/public/svgs/icons/Desktop.svg new file mode 100644 index 0000000..2ad4fbd --- /dev/null +++ b/src/public/svgs/icons/Desktop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Device.svg b/src/public/svgs/icons/Device.svg new file mode 100644 index 0000000..ea62d89 --- /dev/null +++ b/src/public/svgs/icons/Device.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Electronic.svg b/src/public/svgs/icons/Electronic.svg new file mode 100644 index 0000000..7f34f13 --- /dev/null +++ b/src/public/svgs/icons/Electronic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Home.svg b/src/public/svgs/icons/Home.svg new file mode 100644 index 0000000..230751a --- /dev/null +++ b/src/public/svgs/icons/Home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Laptop.svg b/src/public/svgs/icons/Laptop.svg new file mode 100644 index 0000000..eeb9595 --- /dev/null +++ b/src/public/svgs/icons/Laptop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Money.svg b/src/public/svgs/icons/Money.svg new file mode 100644 index 0000000..1476d84 --- /dev/null +++ b/src/public/svgs/icons/Money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Motorcycle.svg b/src/public/svgs/icons/Motorcycle.svg new file mode 100644 index 0000000..bd3776e --- /dev/null +++ b/src/public/svgs/icons/Motorcycle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Other.svg b/src/public/svgs/icons/Other.svg new file mode 100644 index 0000000..31bfd17 --- /dev/null +++ b/src/public/svgs/icons/Other.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Phone.svg b/src/public/svgs/icons/Phone.svg new file mode 100644 index 0000000..b34a759 --- /dev/null +++ b/src/public/svgs/icons/Phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Stock.svg b/src/public/svgs/icons/Stock.svg new file mode 100644 index 0000000..3caf5b6 --- /dev/null +++ b/src/public/svgs/icons/Stock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/more.svg b/src/public/svgs/more.svg new file mode 100644 index 0000000..d0b697f --- /dev/null +++ b/src/public/svgs/more.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/public/svgs/plans.svg b/src/public/svgs/plans.svg new file mode 100644 index 0000000..70fb205 --- /dev/null +++ b/src/public/svgs/plans.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/router/user.js b/src/router/user.js new file mode 100644 index 0000000..e4511ad --- /dev/null +++ b/src/router/user.js @@ -0,0 +1,558 @@ +const getRates = require("../util/exchangeRate"); +const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations"); +const status = require("../util/statuses"); +const ObjectId = require('mongodb').ObjectId; +const session = require("express-session"); +const bcrypt = require("bcrypt"); +const path = require("path"); +const joi = require("joi"); +const fs = require("fs"); +const salt = 12; + +/** + * getAssetSchema returns correct joi object + * to validate req.body depending on asset type + * @param {string} type of asset + * @returns {joi.object} schema + */ +const getAssetSchema = (type) => { + let assetSchema; + + switch (type) { + case "other": + assetSchema = joi.object({ + type: joi.string().valid("other", "stock", "saving").required(), + icon: joi.string().alphanum().required(), + name: joi.string().alphanum().min(3).max(30).required(), + value: joi.number().min(0).required(), + purchaseDate: joi.date().required(), + description: joi.string().alphanum().max(240), + id: joi.string().alphanum(), // May be passed when updating existing asset + }); + break; + case "saving": + assetSchema = joi.object({ + type: joi.string().valid("other", "stock", "saving").required(), + name: joi.string().alphanum().min(3).max(30).required(), + value: joi.number().min(0).required(), + id: joi.string().alphanum(), // May be passed when updating existing asset + }); + break; + case "stock": + assetSchema = joi.object({ + type: joi.string().valid("other", "stock", "saving").required(), + ticker: joi.string().alphanum().min(3).max(5).required(), + price: joi.number().min(0).required(), + quantity: joi.number().min(1).required(), + purchaseDate: joi.date().required(), + id: joi.string().alphanum(), // May be passed when updating existing asset + }); + break; + default: + return null; + }; + + return assetSchema; +} + +/** + * @param {function} middleware handler + * @param {MongoClient.collection} users db collection + * @param {MongoClient.collection} assets db collection + * @returns {express.Router} user protected routes router + */ +module.exports = (middleware, users, plans, assets) => { + const router = require("express").Router(); + + // Create list of icon filenames + let icons = fs.readdirSync(path.join(__dirname, "../public/svgs/icons")); + icons = icons.map((icon) => icon.split(".")[0]); + + router.use(middleware); + + router.get('/home', async (req, res) => { + // if no session with geoData + if (!req.session.geoData) { + req.session.geoData = { + country: null, + toCurrencyRates: [], + }; + } + + res.render('dashboard', { user: req.user, geoData: req.session.geoData }); + + return res.status(status.Ok); + }); + + router.get('/assets', async (req, res) => { + let userAssets = await assets.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + res.render('assets', { + user: req.session.user, + errMessage: req.session.errMessage, + assets: userAssets, + geoData: req.session.geoData, + icons: icons, + }); + return res.status(status.Ok); + }); + + router.get('/plans', async (req, res) => { + try { + const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + + // Use a for...of loop for proper async/await behavior in series for updates + for (const plan of userPlansFromDB) { + const percentage = await calculatePlanProgress(plan, assets, req.session.user._id); + await updatePlanProgressInDB(plan._id, percentage, plans); // Pass the 'plans' collection + } + + // Re-fetch plans to get updated progress for rendering + const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + + res.render('plans', { + user: req.session.user, + plans: updatedUserPlans, // Send the most up-to-date plans + geoData: req.session.geoData + }); + } catch (err) { + console.error("Error fetching plans:", err); + req.session.errMessage = "Could not load your plans. Please try again."; + res.status(status.InternalServerError).redirect('/home'); + } + }); + + router.get('/plans/:id', async (req, res) => { + + try { + const planId = req.params.id; + let userAssets = await assets.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + + + if (!ObjectId.isValid(planId)) { + req.session.errMessage = "Invalid plan ID format."; + return res.status(status.BadRequest).redirect('/plans'); + } + + const plan = await plans.findOne({ userId: new ObjectId(req.session.user._id), _id: new ObjectId(planId) }); + + if (!plan) { + console.log(`Plan not found with ID: ${planId} for user: ${req.session.user.email}`); + req.session.errMessage = "Plan not found or you do not have permission to view it."; + return res.status(status.NotFound).redirect('/plans'); + } + + // The plan.progress should be up-to-date from the database as it was updated in the /plans route + // or when assets/plans are modified. If an immediate recalculation for this specific view is absolutely needed, + // (e.g., if assets were modified without an immediate plan progress update elsewhere), + // you could do it here: + // const currentProgress = await calculatePlanProgress(plan, assets, req.session.user._id); + // plan.progress = currentProgress; // This would only update the 'plan' object for this render, not in DB + + res.render('planDetail', { + user: req.session.user, + plan: plan, // This plan object will have the progress from the database + geoData: req.session.geoData, + assets: userAssets, + }); + + } catch (err) { + console.error("Error fetching plan:", err); + req.session.errMessage = "Could not load your plan. Please try again."; + res.status(status.InternalServerError).redirect('/home'); + } + }); + + router.get('/newPlan', (req, res) => { + if (!req.session.user.financialData || !req.session.user) { + req.session.errMessage = "Please complete your financial data before creating a plan."; + return res.status(status.Unauthorized).redirect('/questionnaire'); + } + const errMessage = req.session.errMessage; + req.session.errMessage = ""; + res.render('newPlan', { + user: req.session.user, + errMessage: errMessage, + geoData: req.session.geoData + }); + }); + + router.post('/newPlan', async (req, res) => { + const planSchema = joi.object({ + name: joi.string().min(3).max(100).required(), + retirementAge: joi.number().min(18).max(120).required(), + retirementExpenses: joi.number().min(0).required(), + retirementAssets: joi.number().min(0).required(), + retirementLiabilities: joi.number().min(0).required(), + }); + + const validationOptions = { convert: true, abortEarly: false }; + const { error, value } = planSchema.validate(req.body, validationOptions); + + if (error) { + console.error("Plan validation error:", error.details); + req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', '); + res.status(status.BadRequest).redirect("/newPlan"); + return; + } + const newPlan = { + userId: new ObjectId(req.session.user._id), + name: value.name, + retirementAge: value.retirementAge, + retirementExpenses: value.retirementExpenses, + retirementAssets: value.retirementAssets, + retirementLiabilities: value.retirementLiabilities, + progress: "0" + }; + + try { + await plans.insertOne({ userId: new ObjectId(req.session.user._id), ...newPlan }); + req.session.errMessage = ""; + res.redirect('/plans'); + } + catch (err) { + console.error("Error saving plan:", err); + req.session.errMessage = "An error occurred while saving your plan. Please try again."; + res.status(status.InternalServerError).redirect("/newPlan"); + } + }); + + router.get('/more', (req, res) => { + res.render('more', { + user: req.session.user, + geoData: req.session.geoData + }); + return res.status(status.Ok); + }); + + router.get('/profile', (req, res) => { + res.render('profile', { + user: req.session.user, + errMessage: req.session.errMessage, + geoData: req.session.geoData + }); + return res.status(status.Ok); + }); + + router.get('/settings', (req, res) => { + res.render('settings', { + user: req.session.user, + geoData: req.session.geoData + }); + return res.status(status.Ok); + }); + + router.get('/questionnaire', (req, res) => { + const errMessage = req.session.errMessage; + req.session.errMessage = ""; + res.render('questionnaire', { + user: req.session.user, + errMessage: errMessage, + geoData: req.session.geoData + }); + }); + + router.post('/questionnaire', (req, res) => { + const questionnaireSchema = joi.object({ + dob: joi.date().required(), + education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(), + maritalStatus: joi.string().valid('single', 'married', 'divorced', 'widowed').required(), + income: joi.number().min(0).required(), + expenses: joi.number().min(0).required(), + assets: joi.number().min(0).required(), + liabilities: joi.number().min(0).required(), + }); + + const validationOptions = { convert: true, abortEarly: false }; + const { error, value } = questionnaireSchema.validate(req.body, validationOptions); + + if (error) { + console.error("Questionnaire validation error:", error.details); + req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', '); + res.status(status.BadRequest).redirect("/questionnaire"); + return; + } + + users.updateOne( + { _id: new ObjectId(req.session.user._id) }, + { + $set: { + financialData: true, + dob: value.dob, + education: value.education, + maritalStatus: value.maritalStatus, + income: value.income, + expenses: value.expenses, + assets: value.assets, + liabilities: value.liabilities, + } + } + ).then((result) => { + if (result.matchedCount === 0) { + console.log(`User not found during questionnaire update: ${req.session.user.email}`); + req.session.errMessage = "User session invalid. Please log in again."; + res.status(status.NotFound).redirect("/login"); + return; + } + if (result.modifiedCount === 0 && result.matchedCount === 1) { + console.log(`User questionnaire data unchanged (already up-to-date): ${req.session.user.email}`); + } + + req.session.user.financialData = true; + req.session.errMessage = ""; + + req.session.save(err => { + if (err) { + res.status(status.InternalServerError).redirect("/plans"); + } + res.status(status.Ok).redirect("/plans"); + }); + + }).catch(err => { + console.error("Error updating questionnaire in database:", err); + req.session.errMessage = "An error occurred while saving your information. Please try again."; + res.status(status.InternalServerError).redirect("/questionnaire"); + }); + }); + + router.post("/updateAccount", async (req, res) => { + const accountSchema = joi.object({ + email: joi.string().email(), + name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20), + password: joi.string().alphanum().max(20).min(8), + repassword: joi.string().alphanum().max(20).min(8), + }); + + const valid = accountSchema.validate(req.body); + + if (valid.err) { + req.session.errMessage = "Invalid input", + res.status(status.BadRequest); + return res.redirect("/profile"); + } + + let update = { + name: req.body.name, + }; + + if ((req.body.password != "") && (req.body.password != req.body.repassword)) { + req.session.errMessage = "New passwords must match"; + res.status(status.BadRequest); + return res.redirect("/profile"); + } else if (req.body.password != "") { + let hashedPassword = await bcrypt.hashSync(req.body.password, salt); + update.password = hashedPassword; + } + + users.updateOne( + { email: req.session.email }, + { $set: update } + ).then((result) => { + if (result.matchedCount === 0) { + console.log(`User not found during account update: ${req.session.email}`); + req.session.errMessage = "User session invalid. Please log in again."; + res.status(status.NotFound).redirect("/login"); + return; + } + if (result.modifiedCount === 0 && result.matchedCount === 1) { + console.log(`User account data unchanged (already up-to-date): ${req.session.email}`); + } + + req.session.errMessage = ""; + return res.status(status.Ok).redirect("/profile"); + }).catch(err => { + console.error("Error updating account in database:", err); + req.session.errMessage = "An error occurred while saving your information. Please try again."; + return res.status(status.InternalServerError).redirect("/profile"); + }); + }); + + router.post("/createAsset", async (req, res) => { + // Create asset, each asset has different data structure based on type + const type = req.body.type; + const assetSchema = getAssetSchema(type); + + if (!assetSchema) { + console.error("Modified asset type, rejected"); + req.session.errMessage = "Invalid input"; + return res.status(status.BadRequest).redirect("/assets"); + } + + const valid = assetSchema.validate(req.body); + + if (valid.err) { + req.session.errMessage = "Invalid input", + res.status(status.BadRequest); + return res.redirect("/assets"); + } + + let newAsset = { + userId: new ObjectId(req.session.user._id), + ...req.body, + updatedAt: new Date(), + }; + + if (type == "stock") { + newAsset.quantity = parseInt(newAsset.quantity); + newAsset.price = parseFloat(newAsset.price) + newAsset.value = newAsset.quantity * newAsset.price; + newAsset.name = `${newAsset.ticker} Stock`; + } + newAsset.value = parseFloat(newAsset.value); + newAsset.icon = type == "stock" ? "Stock" : type == "saving" ? "Coins" : newAsset.icon; + + assets.insertOne(newAsset, (err, _) => { + if (err) { + console.error("Error creating asset: ", err); + req.session.errMessage = "Internal server error"; + return res.status(status.InternalServerError).redirect("/assets"); + } + }); + + req.session.errMessage = ""; + return res.status(status.Ok).redirect("/assets"); + }); + + router.post("/updateAsset", async (req, res) => { + const type = req.body.type; + const assetSchema = getAssetSchema(type); + + if (!assetSchema) { + console.error("Modified asset type, rejected"); + req.session.errMessage = "Invalid input"; + return res.status(status.BadRequest).redirect("/assets"); + } + + const valid = assetSchema.validate(req.body); + + if (valid.err) { + req.session.errMessage = "Invalid input", + res.status(status.BadRequest); + return res.redirect("/assets"); + } + + if (req.body.userId != req.session.user._id) { + req.session.errMessage = "Cannot change asset owner", + res.status(status.BadRequest); + return res.redirect("/assets"); + } + + let update = { ...req.body, updatedAt: new Date() }; + if (type == "stock") { + update.quantity = parseInt(update.quantity); + update.price = parseFloat(update.price) + update.value = update.quantity * update.price; + update.name = `${update.ticker} Stock`; + } + update.value = parseFloat(update.value); + delete update.id; + delete update.userId; + + assets.updateOne( + { "_id": new ObjectId(req.body.id) }, + { $set: update }, + ).then((result) => { + if (result.matchedCount === 0) { + console.log(`Asset not found: ${req.body.id}`); + req.session.errMessage = "Unable to update asset"; + return res.status(status.NotFound).redirect("/assets"); + } + if (result.modifiedCount === 0 && result.matchedCount === 1) { + console.log(`Asset data unchanged (already up-to-date): ${req.body.id}`); + } + + req.session.errMessage = ""; + return res.status(status.Ok).redirect("/assets"); + + }).catch((err) => { + console.error("Error updating asset: ", err); + req.session.errMessage = "An error occurred while saving your information. Please try again."; + return res.status(status.InternalServerError).redirect("/assets"); + }); + }); + + router.post("/deleteAsset", (req, res) => { + const id = new ObjectId(req.body.id); + + assets.deleteOne( + { "_id": id } + ).then((result) => { + if (result.deletedCount === 0) { + console.error(`Asset not found: ${req.body.id}`); + req.session.errMessage = "Unable to delete asset. Please try again."; + return res.status(status.NotFound).redirect("/assets"); + } + + if (!result.acknowledged) { + console.error("Error deleting asset: ", err); + req.session.errMessage = "An error occurred while deleting an asset. Please try again."; + return res.status(status.InternalServerError).redirect("/assets"); + } + + req.session.errMessage = ""; + return res.status(status.Ok).redirect("/assets"); + + }).catch((err) => { + console.error("Error deleting asset: ", err); + req.session.errMessage = "An error occurred while deleting an asset. Please try again."; + return res.status(status.InternalServerError).redirect("/assets"); + }); + }); + + router.post("/deleteUser", (req, res) => { + // not as critical if results aren't as expected only if crashing + assets.deleteMany({ userId: new ObjectId(req.session.user._id) }).catch((err) => { + console.error("Error deleting user assets: ", err); + req.session.errMessage = "An error occured while deleting your account. Please try again."; + return res.status(status.InternalServerError).redirect("/profile"); + }); + + plans.deleteMany({ userId: new ObjectId(req.session.user._id) }).catch((err) => { + console.error("Error deleting user assets: ", err); + req.session.errMessage = "An error occured while deleting your account. Please try again."; + return res.status(status.InternalServerError).redirect("/profile"); + }); + + users.deleteOne( + { _id: new ObjectId(req.session.user._id) }, + ).then((result) => { + if (result.deletedCount === 0) { + console.error(`User not found: ${req.body.id}`); + req.session.errMessage = "Unabled to delete account. Please try again."; + return res.status(status.NotFound).redirect("/profile"); + } + + if (!result.acknowledged) { + console.error("Error deleting user: ", err); + req.session.errMessage = "An error occured while deleting your account. Please try again."; + return res.status(status.InternalServerError).redirect("/profile"); + } + + // Direct to logout to destroy session + req.session.destroy(); + return res.status(status.Ok).redirect("/signup"); + }).catch((err) => { + console.error("Error deleting user: ", err); + req.session.errMessage = "An error occured while deleting your account. Please try again."; + return res.status(status.InternalServerError).redirect("/profile"); + }); + }); + + router.get("/exRates/:lat/:lon", async (req, res) => { + if (!req.session.geoData.country) { + const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${req.params.lat},${req.params.lon}&result_type=country&key=${process.env.GEOLOCATION_API}`); + const data = await response.json(); + + country = data.results[0].formatted_address; + let results = await getRates(country); + req.session.geoData = { + country: results.abbreviation, + toCurrencyRates: results.exRates, + geoData: req.session.geoData + } + } + + return res.status(status.Ok).send({ data: req.session.geoData }); + }) + + return router; +}; diff --git a/src/util/calculations.js b/src/util/calculations.js new file mode 100644 index 0000000..25f5d33 --- /dev/null +++ b/src/util/calculations.js @@ -0,0 +1,45 @@ +const ObjectId = require('mongodb').ObjectId; + +async function calculatePlanProgress(plans, assets, userId) { + if (!plans || typeof plans.retirementAssets === 'undefined') { + console.error("Invalid plan document provided to calculatePlanProgress:", plans); + return 0; + } + if (!assets || typeof assets.find !== 'function') { + console.error("Invalid assetsCollection provided to calculatePlanProgress"); + return 0; + } + + try { + const userAssets = await assets.find({ userId: new ObjectId(userId) }).toArray(); + const totalUserAssetValue = userAssets.reduce((total, asset) => total + asset.value, 0); + let percentage = 0; + + if (plans.retirementAssets > 0) { + percentage = (totalUserAssetValue / plans.retirementAssets) * 100; + } + return percentage; + } catch (err) { + console.error("Error in calculatePlanProgress:", err); + return 0; + } +} + +async function updatePlanProgressInDB(planId, percentage, plans) { + if (!plans || typeof plans.updateOne !== 'function') { + console.error("Error with the plans collection"); + return; + } + if (typeof percentage !== 'number' || isNaN(percentage)) { + console.error(`Error with the percentage: ${percentage}`); + return; + } + + try { + await plans.updateOne({ _id: new ObjectId(planId) }, { $set: { progress: parseFloat(percentage.toFixed(2)) } }); + } catch (err) { + console.error("Error in updatePlanProgressInDB:", err); + } +} + +module.exports = { calculatePlanProgress, updatePlanProgressInDB }; \ No newline at end of file diff --git a/src/util/countries.json b/src/util/countries.json new file mode 100644 index 0000000..039a74c --- /dev/null +++ b/src/util/countries.json @@ -0,0 +1,132 @@ +{ + "countries": [ + { + "country": "United States", + "currency_abbreviation": "USD" + }, + { + "country": "Eurozone", + "currency_abbreviation": "EUR" + }, + { + "country": "United Kingdom", + "currency_abbreviation": "GBP" + }, + { + "country": "Japan", + "currency_abbreviation": "JPY" + }, + { + "country": "Switzerland", + "currency_abbreviation": "CHF" + }, + { + "country": "Canada", + "currency_abbreviation": "CAD" + }, + { + "country": "Australia", + "currency_abbreviation": "AUD" + }, + { + "country": "China", + "currency_abbreviation": "CNY" + }, + { + "country": "India", + "currency_abbreviation": "INR" + }, + { + "country": "Russia", + "currency_abbreviation": "RUB" + }, + { + "country": "Brazil", + "currency_abbreviation": "BRL" + }, + { + "country": "Mexico", + "currency_abbreviation": "MXN" + }, + { + "country": "South Korea", + "currency_abbreviation": "KRW" + }, + { + "country": "Indonesia", + "currency_abbreviation": "IDR" + }, + { + "country": "Bulgaria", + "currency_abbreviation": "BGN" + }, + { + "country": "Czech Republic", + "currency_abbreviation": "CZK" + }, + { + "country": "Denmark", + "currency_abbreviation": "DKK" + }, + { + "country": "Hong Kong", + "currency_abbreviation": "HKD" + }, + { + "country": "Hungary", + "currency_abbreviation": "HUF" + }, + { + "country": "Israel", + "currency_abbreviation": "ILS" + }, + { + "country": "Iceland", + "currency_abbreviation": "ISK" + }, + { + "country": "Malaysia", + "currency_abbreviation": "MYR" + }, + { + "country": "Norway", + "currency_abbreviation": "NOK" + }, + { + "country": "New Zealand", + "currency_abbreviation": "NZD" + }, + { + "country": "Philippines", + "currency_abbreviation": "PHP" + }, + { + "country": "Poland", + "currency_abbreviation": "PLN" + }, + { + "country": "Romania", + "currency_abbreviation": "RON" + }, + { + "country": "Sweden", + "currency_abbreviation": "SEK" + }, + { + "country": "Singapore", + "currency_abbreviation": "SGD" + }, + { + "country": "Thailand", + "currency_abbreviation": "THB" + }, + { + "country": "Turkey", + "currency_abbreviation": "TRY" + }, + { + "country": "South Africa", + "currency_abbreviation": "ZAR" + } + ] +} \ No newline at end of file diff --git a/src/util/exchangeRate.js b/src/util/exchangeRate.js new file mode 100644 index 0000000..2f5658d --- /dev/null +++ b/src/util/exchangeRate.js @@ -0,0 +1,18 @@ +const countries = require("./countries.json").countries; + +async function getRates(country) { + let abbr; + countries.forEach(c => { + if (c.country === country) { + abbr = c.currency_abbreviation; + } + }); + + const res = await fetch(`https://api.frankfurter.dev/v1/latest?base=${abbr}`); + const data = await res.json(); + let rates = data.rates; + + return { exRates: rates, abbreviation: abbr }; +} + +module.exports = getRates; \ No newline at end of file diff --git a/src/util/statuses.js b/src/util/statuses.js new file mode 100644 index 0000000..d06ffba --- /dev/null +++ b/src/util/statuses.js @@ -0,0 +1,7 @@ +module.exports = { + Ok: 200, + BadRequest: 400, + Unauthorized: 401, + NotFound: 404, + InternalServerError: 500, +}; diff --git a/src/views/aboutUs.ejs b/src/views/aboutUs.ejs new file mode 100644 index 0000000..32ded54 --- /dev/null +++ b/src/views/aboutUs.ejs @@ -0,0 +1,39 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/headerStart") %> + +
+
+
+

About RCalculator

+

Welcome to RCalculator, your partner in building a secure and fulfilling financial future.

+
+ +
+

Our Mission

+

+ Our mission is to empower you with the tools and insights needed to take control of your long-term financial goals. +

+
+

+ We believe that planning for your desired future, especially retirement, shouldn't be daunting. It doesn't matter what your current age or career status is – starting now is what counts. +

+

+ At RCalculator, we help you trace a personalized plan that suits your unique aspirations. We provide clear, actionable advice and projections to guide you on how to get there, whether you're just starting your career, mid-way through, or nearing retirement. +

+

+ Let us help you navigate the path to financial independence and achieve the retirement lifestyle you envision. +

+
+
+ +
+
+ +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/assets.ejs b/src/views/assets.ejs new file mode 100644 index 0000000..764e6bd --- /dev/null +++ b/src/views/assets.ejs @@ -0,0 +1,392 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+ + +
+

Create New Asset

+
+ +
+
+ +
+ +
+ + + +
+
+ + +
+ + + + +
+ + +
+ + + + + + + + + + + + + +
+ + +
+
+ + + + + + +
+ + <% if (errMessage != "") { %> +
<%= errMessage %>
+ <% } %> + + +
+
+
+

Assets:

+

<%= assets.length %>

+
+
+

Total Value:

+

+ + $<%= assets.reduce((total, e) => total + e.value, 0).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> +

+
+
+ +
+ + Back to Dashboard + + +
+
+ +
+ + +
+ <% if (assets.length == 0) { %> +
+ You have no assets, create a new asset. +
+ <% } %> + <% assets.forEach((asset) => { %> + +
+ +
+ + + +
+ +
+
+ +
+ +

Are you sure you want to delete this item?

+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+ +
+
+
+ +
+ <% if (asset.type == "other") { %> +
+ + +
+ <% } else { %> + + <% } %> + +

<%= String(asset.type).charAt(0).toUpperCase() + String(asset.type).slice(1); %> Asset

+
+ +
+
+
+ +
+ + + + + + <% if (asset.type != "stock") { %> + + + <% } else { %> + + + <% } %> + +
+ + <% if (asset.type != "stock") { %> + + + <% } else { %> + + + + + + <% } %> + + <% if (asset.type == "other") { %> + + + <% } %> + + <% if (asset.type != "saving") { %> + + + <% } %> + +
+ Last Modified: <%= asset.updatedAt %> +
+ +
+ +
+
+
+ <% }) %> +
+
+ +
+ + + + +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs new file mode 100644 index 0000000..86539d0 --- /dev/null +++ b/src/views/dashboard.ejs @@ -0,0 +1,116 @@ +<%- include("./partials/fileHeader") %> + <%- include("./partials/header") %> + +
+ +
+ Add Asset + Create new plan +
+ +
+ + + + + +
+
+ +
+ + + +
+

+ Retirement goal

+

+ $53k

+
+
+

+ Make this percent bar or graph or + something +  than lastweek +

+
+
+
+
+

+ Retirement goal

+

+ $53k

+
+
+

+ Make this percent bar or graph or + something +  than last + week +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

+ Retirement goal

+

+ $53k

+
+
+

+ Make this percent bar or + graph or something +  than last + week +

+
+
+
+
+
+
+
+
+ + <%- include("./partials/navBar") %> + <%- include("./partials/scriptLoader") %> + <%- include("./partials/footer") %> diff --git a/src/views/forgotPass.ejs b/src/views/forgotPass.ejs new file mode 100644 index 0000000..d2d749f --- /dev/null +++ b/src/views/forgotPass.ejs @@ -0,0 +1,38 @@ +<%- include("./partials/fileHeader") %> + +
+
+
+
+

Reset Password

+
+ <% if (!reset ) { %> +
+ + +
+ <% } else { %> +
+

Reset link sent.

+
+ <% } %> + + +
+ <%= error%> +
+ <% if (!reset) { %> +
+ +
+ <% } %> +
+
+
+
+ + <%- include("./partials/footer") %> <%- include("./partials/footer") %> diff --git a/src/views/landing.ejs b/src/views/landing.ejs new file mode 100644 index 0000000..d7a24d1 --- /dev/null +++ b/src/views/landing.ejs @@ -0,0 +1,24 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/headerStart") %> + +
+
+
+

Plan now, live better.

+

Planning your retirement is a crucial step towards financial security and a happy future.

+ +
+
+
+ +<%- include("./partials/footer") %> diff --git a/src/views/login.ejs b/src/views/login.ejs new file mode 100644 index 0000000..d35800f --- /dev/null +++ b/src/views/login.ejs @@ -0,0 +1,44 @@ +<%- include("./partials/fileHeader") %> + +
+
+
+
+

Login

+
+
+ + +
+
+ + +
+
<%= errMessage %>
+
+
+ + +
+ +
+
+ +
+
+ + Register +
+
+
+
+
+ +<%- include("./partials/footer") %> diff --git a/src/views/more.ejs b/src/views/more.ejs new file mode 100644 index 0000000..934700e --- /dev/null +++ b/src/views/more.ejs @@ -0,0 +1,11 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+ The More Page +
+ +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> + \ No newline at end of file diff --git a/src/views/newPlan.ejs b/src/views/newPlan.ejs new file mode 100644 index 0000000..5370290 --- /dev/null +++ b/src/views/newPlan.ejs @@ -0,0 +1,42 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+

Welcome: <%= user.name %>

+
+

Retirement Plan

+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/notFound.ejs b/src/views/notFound.ejs new file mode 100644 index 0000000..2e783bc --- /dev/null +++ b/src/views/notFound.ejs @@ -0,0 +1,24 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/headerStart") %> + +
+
+
+

404 - Not found

+

It appears you stumbled accross a misleading page.

+ +
+
+
+ +<%- include("./partials/footer") %> diff --git a/src/views/partials/fileHeader.ejs b/src/views/partials/fileHeader.ejs new file mode 100644 index 0000000..b15b69c --- /dev/null +++ b/src/views/partials/fileHeader.ejs @@ -0,0 +1,10 @@ + + + + + + + + RCalculator + + \ No newline at end of file diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs new file mode 100644 index 0000000..3a39799 --- /dev/null +++ b/src/views/partials/footer.ejs @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs new file mode 100644 index 0000000..90410ca --- /dev/null +++ b/src/views/partials/header.ejs @@ -0,0 +1,95 @@ + +
+ +
\ No newline at end of file diff --git a/src/views/partials/headerStart.ejs b/src/views/partials/headerStart.ejs new file mode 100644 index 0000000..9b215d4 --- /dev/null +++ b/src/views/partials/headerStart.ejs @@ -0,0 +1,32 @@ +
+ +
\ No newline at end of file diff --git a/src/views/partials/navBar.ejs b/src/views/partials/navBar.ejs new file mode 100644 index 0000000..3097ffe --- /dev/null +++ b/src/views/partials/navBar.ejs @@ -0,0 +1,20 @@ +
+
+ + dashboard + Dashboard + + + assets + Assets + + + plans + Plans + + + more + More + +
+
\ No newline at end of file diff --git a/src/views/partials/scriptLoader.ejs b/src/views/partials/scriptLoader.ejs new file mode 100644 index 0000000..2ed92a3 --- /dev/null +++ b/src/views/partials/scriptLoader.ejs @@ -0,0 +1,7 @@ + + + diff --git a/src/views/planDetail.ejs b/src/views/planDetail.ejs new file mode 100644 index 0000000..e75b46f --- /dev/null +++ b/src/views/planDetail.ejs @@ -0,0 +1,75 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+

Welcome: <%= user.name %>

+
+ +
+

<%= plan.name %>

+
+ +
+
+ Progress + <%= plan.progress %>% +
+
+
+
+
+ +
+

Assets:

+ +
+
+ +

<%= assets.length %>

+
+
+ +

<%= assets.reduce((total, asset) => total + asset.value, 0) %>

+
+
+
+ +
+

Plan Details:

+ +
+
+ +

<%= plan.retirementAge %>

+
+
+ +

<%= plan.retirementExpenses %>

+
+
+ +

<%= plan.retirementAssets %>

+
+
+ +

<%= plan.retirementLiabilities %>

+
+
+
+ +
+

Suggestions:

+ +
+

+ USE YOUR HEAD MOCK +

+
+
+ +
+
+ +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> diff --git a/src/views/plans.ejs b/src/views/plans.ejs new file mode 100644 index 0000000..da1c0ef --- /dev/null +++ b/src/views/plans.ejs @@ -0,0 +1,25 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+

Welcome: <%= user.name %>

+
+

My Retirement Plans

+
New Plan
+
+ <% plans.forEach(plan => { %> + +
<%= plan.name %>
+
+
+
+

<%= plan.description %>

+
+ <% }) %> +
+ +
+ +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> diff --git a/src/views/profile.ejs b/src/views/profile.ejs new file mode 100644 index 0000000..8d6e3b5 --- /dev/null +++ b/src/views/profile.ejs @@ -0,0 +1,142 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+ + +
+ +
+
+ +
+ +

Are you sure you want to delete you account? This process cannot be undone.

+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+
+ +

Welcome <%= user.name %>

+ + <% if (errMessage != "") { %> +
<%= errMessage %>
+ <% } %> + +
+
+

Account settings

+ +
+
+ + + + + + + + + + + + + +
+ +
+
+
+ +
+ +
+ + + +
+
+ + + +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs new file mode 100644 index 0000000..84420e6 --- /dev/null +++ b/src/views/questionnaire.ejs @@ -0,0 +1,73 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+

Welcome: <%= user.name %>

+
+

Financial Questionnaire

+
+
+ + +
+
+ + +
+
+ +
+ + + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/resetPass.ejs b/src/views/resetPass.ejs new file mode 100644 index 0000000..b608e5a --- /dev/null +++ b/src/views/resetPass.ejs @@ -0,0 +1,36 @@ +<%- include("./partials/fileHeader") %> + +
+
+
+
+

Reset Password

+
+ +
+ + +
+
+ + +
+ <% if (typeof errMessage !=='undefined' ) { %> +
+ <%= errMessage %> +
+ <% } %> +
+ +
+
+
+
+
+ <%- include("./partials/footer") %> diff --git a/src/views/signup.ejs b/src/views/signup.ejs new file mode 100644 index 0000000..ac2d8ac --- /dev/null +++ b/src/views/signup.ejs @@ -0,0 +1,46 @@ +<%- include("./partials/fileHeader") %> + <%- include("./partials/headerStart") %> + +
+
+
+
+

Signup

+
+
+ + +
+
+ + +
+
+ + + + +
+
+ <%= errMessage %> +
+
+ +
+
+ + Login +
+
+
+ + <%- include("./partials/footer") %> diff --git a/src/views/template.ejs b/src/views/template.ejs new file mode 100644 index 0000000..868ca17 --- /dev/null +++ b/src/views/template.ejs @@ -0,0 +1,9 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+ +
+ +<%- include("./partials/navBar") %> +<%- include("./partials/footer") %> \ No newline at end of file