diff --git a/.env.example b/.env.example index 518df8a..aaa7118 100755 --- a/.env.example +++ b/.env.example @@ -1,4 +1,8 @@ -mongoURI='mongodb://localhost:27017/' -database='nameOfDatabase' +MONGO_URI='mongodb://localhost:27017/' +DATABASE='nameOfDatabase' PORT=8000 -secret='123456789' \ No newline at end of file +SECRET='123456789' +GEOLOCATION_API='api_key' +EMAIL_USER=mail@example.com +EMAIL_PASS='password' +GOOGLE_API_KEY='api_key' 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/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 index 0928bee..741673f 100644 --- a/app.js +++ b/app.js @@ -3,16 +3,16 @@ 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.mongoURI || "mongodb://localhost:27017/"; -const database = process.env.database || "knoldus"; // Database name -const secret = process.env.secret || "123-secret-xyz"; +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({ @@ -40,9 +40,9 @@ async function initDatabase() { const db = await connectMongo(mongoURI, database); // For any collection, init here - users = await getCollection(db, "users"); + users = await getCollection(db, "users"); assets = await getCollection(db, "assets"); - plans = await getCollection(db, "plans"); + plans = await getCollection(db, "plans"); } /*** ROUTINGS ***/ @@ -74,17 +74,39 @@ app.get('/aboutUs', (req, res) => { return res.status(status.Ok); }); -app.post('/api/location', async (req,res) => { - const { latitude, longitude } = req.body; +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); +}); - const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&result_type=country&key=${process.env.geolocation_api}`); +// Reset with token given to user via email +app.get('/reset/:token', async (req, res) => { + const token = req.params.token; - const data = await response.json(); + const user = await users.findOne({ + resetToken: token, + resetTokenExpires: { $gt: Date.now() }, + }); - res.json(data); + 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); @@ -96,7 +118,9 @@ initDatabase().then(() => { // 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)); @@ -106,7 +130,7 @@ initDatabase().then(() => { 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 index 95750e3..be5d1bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,38 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@google/genai": "^0.14.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" + "mongodb": "^6.16.0", + "nodemailer": "^7.0.3", + "sweetalert2": "^11.21.0" }, "devDependencies": { "nodemon": "^3.1.10" } }, + "node_modules/@google/genai": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.14.0.tgz", + "integrity": "sha512-nxXbRJZjCUBlPlfTXBMCgkSEpojcLzjxT2Ye3CDiqlXaiSscZ046bDEzztq6ONxUT3BfqHcGsQLhpbR5DU1Mcg==", + "dependencies": { + "google-auth-library": "^9.14.2", + "test-server-sdk": "^0.2.1", + "ws": "^8.18.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -87,6 +106,15 @@ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "license": "BSD-3-Clause" }, + "node_modules/@types/node": { + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "optional": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -102,6 +130,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -209,12 +246,46 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "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==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -229,6 +300,14 @@ "node": ">= 10.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -300,6 +379,19 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -415,6 +507,17 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -483,6 +586,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", @@ -500,6 +609,14 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -550,6 +667,14 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -586,6 +711,14 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -616,6 +749,20 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -713,6 +860,38 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -773,6 +952,58 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -866,6 +1097,71 @@ "node": ">=10" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -903,6 +1199,20 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -937,6 +1247,43 @@ "node": ">= 6" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -949,6 +1296,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -970,6 +1329,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", @@ -1123,6 +1496,17 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -1154,6 +1538,33 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kruptein": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.7.tgz", @@ -1430,6 +1841,15 @@ "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", @@ -1598,6 +2018,11 @@ "node": ">=16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1624,6 +2049,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -1631,6 +2061,15 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1990,6 +2429,15 @@ "node": ">=8" } }, + "node_modules/sweetalert2": { + "version": "11.21.0", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.21.0.tgz", + "integrity": "sha512-fiEK7SqRY/QD/wC2uqEHlfYGZ7qe2UcyQbJpbpj4YRVqplBgcI+euPZLZL+evLINcvbtXmL1SFUdZHKqBHGAAQ==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -2007,6 +2455,17 @@ "node": ">=10" } }, + "node_modules/test-server-sdk": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/test-server-sdk/-/test-server-sdk-0.2.1.tgz", + "integrity": "sha512-o9F0HEBZbdDMwZ5xwSafkVb0C2nlZ4hq/g/aMgyLniTs69J7zoQJzF5eA3ZN6MPRoYPhy/QsQ/BuRtqaXzawvw==", + "hasInstallScript": true, + "dependencies": { + "axios": "^1.6.0", + "extract-zip": "^2.0.1", + "tar": "^6.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2084,6 +2543,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "optional": true + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2099,6 +2564,18 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2145,11 +2622,56 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "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" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index 4f1048f..597241f 100755 --- a/package.json +++ b/package.json @@ -18,14 +18,18 @@ }, "homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme", "dependencies": { + "@google/genai": "^0.14.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" + "mongodb": "^6.16.0", + "nodemailer": "^7.0.3", + "sweetalert2": "^11.21.0" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/src/auth/authentication.js b/src/auth/authentication.js index a540751..d32f0a8 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -12,15 +12,23 @@ module.exports = (users) => { const router = require("express").Router(); router.get("/logout", (req, res) => { - req.session.destroy(); - return res.redirect('/login'); + req.session.destroy((err) => { + if (err) { + console.error("Failed to destroy session: ", err); + res.status(status.InternalServerError); + return res.redirect("back"); + } + + res.status(status.Ok); + return res.redirect('/login'); + }); }); - + router.post("/login", async (req, res) => { const credentialSchema = joi.object({ email: joi.string().email().required(), - password: joi.string().max(20).required(), + password: joi.string().alphanum().max(20).required(), }); const valid = credentialSchema.validate(req.body); @@ -46,19 +54,28 @@ module.exports = (users) => { 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); + + req.session.save((err) => { + if (err) { + console.error("Failed to save session: ", err); + req.session.errMessage = "Failed to save session. Please try again."; + res.status(status.InternalServerError); + return res.redirect("/login"); + } + + res.status(status.Ok); + return res.redirect("/home"); + }); }); }); router.post("/signup", async (req, res) => { const userSchema = joi.object({ email: joi.string().email().required(), - name: joi.string().alphanum().max(20).required(), - password: joi.string().max(20).min(8).required(), - repassword: joi.string().max(20).min(8).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); @@ -69,6 +86,13 @@ module.exports = (users) => { 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); @@ -84,17 +108,26 @@ module.exports = (users) => { financialData: false, }).then((results, err) => { if (err) { - console.error(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"); + + req.session.save((err) => { + if (err) { + console.error("Failed to save session: ", err); + req.session.errMessage = "Please login"; + res.status(status.Ok); + return res.redirect("/login"); + } + + res.status(status.Ok); + return res.redirect("/home"); + }); }); }); diff --git a/src/auth/forgotPass.js b/src/auth/forgotPass.js new file mode 100644 index 0000000..e0022c3 --- /dev/null +++ b/src/auth/forgotPass.js @@ -0,0 +1,119 @@ +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, + } +}); + +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"); // nice + 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 index bf1ed0a..05f5d5e 100644 --- a/src/auth/middleware.js +++ b/src/auth/middleware.js @@ -1,7 +1,8 @@ const status = require("../util/statuses"); const session = require("express-session"); +const ObjectId = require("mongodb").ObjectId; -// Get all user routes names +// 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]); @@ -12,31 +13,58 @@ const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1 */ const createMiddleware = (users) => { return async (req, res, next) => { - // Redirect not found pages to 404 page - if (!userRoutes.includes(req.url.substring(1))) { + // 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) { + if (!req.session.authenticated || !req.session.userId) { 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); - + let user = await users.findOne({ _id: new ObjectId(req.session.userId) }).then((user) => user); if (!user) { - req.session.errMessage = "User not found"; - res.redirect("/login"); - return res.status(status.Unauthorized); + return req.session.destroy((err) => { + if (err) { + console.error("Failed to destroy session: ", err); + req.session.errMessage = "Failed to logout. Please try again"; + res.status(status.InternalServerError); + return res.redirect("/home"); + } + + req.session.errMessage = "User not found"; + res.status(status.NotFound); + res.redirect("/login"); + }); } - + req.session.user = user; + return req.session.save((err) => { + if (err) { + console.error("Failed to save session: ", err); + + return req.session.destroy((err) => { + req.session.errMessage = "Failed to save session, please login again."; + + if (err) { + console.error("Failed to destroy session: ", err); + } + + res.status(status.InternalServerError); + res.redirect("/login"); + }); + } + + next(); + }); } next(); }; -} +}; module.exports = createMiddleware; diff --git a/src/public/scripts/assetManager.js b/src/public/scripts/assetManager.js index 86ddd08..16bbc37 100644 --- a/src/public/scripts/assetManager.js +++ b/src/public/scripts/assetManager.js @@ -8,6 +8,12 @@ const assetForms = [ * 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(); @@ -40,6 +46,7 @@ function resetRadio() { const assetKeys = { other: [ "name", + "dropdown-icon-button", "value", "description", "purchaseDate", @@ -59,8 +66,17 @@ const assetKeys = { /** * lockAsset prevents edits to asset view modal * @param {string} assetId + * @param {string} icon to defualt to */ -function lockAsset(assetId) { +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; @@ -74,14 +90,15 @@ function lockAsset(assetId) { document.getElementById(`save-${assetId}`).classList.add("cursor-not-allowed"); document.getElementById(`edit-${assetId}`).innerHTML = "Edit"; - document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId) }; + 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) { +function unlockAsset(assetId, icon) { const type = document.getElementById(`type-${assetId}`).value; assetKeys[type].forEach((key) => { @@ -92,8 +109,40 @@ function unlockAsset(assetId) { document.getElementById(`save-${assetId}`).classList.remove("cursor-not-allowed"); document.getElementById(`save-${assetId}`).classList.add("cursor-pointer"); - document.getElementById(`edit-${assetId}`).innerHTML = "Cancel changes"; - document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId) }; + 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/footer.js b/src/public/scripts/footer.js new file mode 100644 index 0000000..7bdc130 --- /dev/null +++ b/src/public/scripts/footer.js @@ -0,0 +1,27 @@ +const factButton = document.getElementById("factButton"); +const factMenu = document.getElementById("factMenu"); +const factInput = document.getElementById("factInput"); +const factSubmitButton = document.getElementById("factSubmitButton"); + +factButton.addEventListener("click", () => { + factMenu.classList.toggle("hidden"); +}); +factSubmitButton.addEventListener("click", () => { + fetch("/fact", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fact: factInput.value, + }), + }) + .then(response => response.json()) + .then(data => { + Swal.fire('Here is a fact!', data.fact, 'success'); + }) + .catch(error => { + console.error("Error fetching fact:", error); + Swal.fire('Error fetching fact', 'Please try again later', 'error'); + }); +}); \ No newline at end of file diff --git a/src/public/scripts/geolocation.js b/src/public/scripts/geolocation.js index ebe001b..94c9c66 100644 --- a/src/public/scripts/geolocation.js +++ b/src/public/scripts/geolocation.js @@ -1,24 +1,84 @@ function getLocation() { if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(success, error); + navigator.geolocation.getCurrentPosition(getLatestExchange, error); } } -function success(position) { - const { latitude, longitude } = position.coords; +function update(data) { + if (data.data.message != "error") { + 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); + }); + } else { + document.getElementById("loading").style = "display: none"; + document.getElementById("exFrom").style = "display: none"; + document.getElementById("dropdown-country-button").style = "display: none"; + document.getElementById("currencyExchange").style = "display: flex"; + document.getElementById("exchange").innerHTML = "Country not supported"; + document.getElementById("exchange").className = "block rounded-lg text-center p-2.5 w-50 z-20 text-sm border bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-blue-500"; + } +} - fetch("api/location" , { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ latitude, longitude }) - }).then(res => res.json()) - .then(data => { - console.log(data.results[0].formatted_address); - }) +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); -} + const data = { + data: { + message: "error" + } + } + + update(data); +} \ No newline at end of file diff --git a/src/public/scripts/profile.js b/src/public/scripts/profile.js index d803378..79f4db0 100644 --- a/src/public/scripts/profile.js +++ b/src/public/scripts/profile.js @@ -11,6 +11,7 @@ function lockAccount() { document.getElementById("password").disabled = true; document.getElementById("repassword").disabled = true; document.getElementById("save-account").classList.add("cursor-not-allowed"); + document.getElementById("save-account").classList.remove("cursor-pointer"); document.getElementById("edit-account").innerHTML = "Edit"; document.getElementById("edit-account").onclick = unlockAccount; @@ -30,7 +31,12 @@ function lockPersonal() { document.getElementById("ms-married").disabled = true; document.getElementById("ms-divorced").disabled = true; document.getElementById("ms-widowed").disabled = true; + document.getElementById("income").disabled = true; + document.getElementById("expenses").disabled = true; + document.getElementById("assets").disabled = true; + document.getElementById("liabilities").disabled = true; document.getElementById("save-personal").classList.add("cursor-not-allowed"); + document.getElementById("save-personal").classList.remove("cursor-pointer"); document.getElementById("edit-personal").innerHTML = "Edit"; document.getElementById("edit-personal").onclick = unlockPersonal; @@ -42,11 +48,12 @@ function lockPersonal() { */ function unlockAccount() { document.getElementById("save-account").disabled = false; - // document.getElementById("email").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("save-account").classList.add("cursor-pointer"); document.getElementById("edit-account").innerHTML = "Cancel changes"; document.getElementById("edit-account").onclick = lockAccount; @@ -64,7 +71,12 @@ function unlockPersonal() { document.getElementById("ms-married").disabled = false; document.getElementById("ms-divorced").disabled = false; document.getElementById("ms-widowed").disabled = false; + document.getElementById("income").disabled = false; + document.getElementById("expenses").disabled = false; + document.getElementById("assets").disabled = false; + document.getElementById("liabilities").disabled = false; document.getElementById("save-personal").classList.remove("cursor-not-allowed"); + document.getElementById("save-personal").classList.add("cursor-pointer"); document.getElementById("edit-personal").innerHTML = "Cancel changes"; document.getElementById("edit-personal").onclick = lockPersonal; @@ -72,4 +84,4 @@ function unlockPersonal() { // On page load, ensure forms are locked and reset lockAccount(); -// lockPersonal(); +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 index 43cfe56..420eb55 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -1,8 +1,13 @@ +const getRates = require("../util/exchangeRate"); +const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations"); +const suggestions = require("../util/suggestions"); 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; /** @@ -18,6 +23,7 @@ const getAssetSchema = (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(), @@ -58,28 +64,55 @@ const getAssetSchema = (type) => { */ 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) => { - res.render('dashboard', { user: req.session.user }); + // 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 }); + let userAssets = await assets.find({ userId: new ObjectId(req.session.userId) }).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 { - // console.log(new ObjectId(req.session.user._id)); - const userPlans = await plans.find({userId: new ObjectId(req.session.user._id) }).toArray(); - // console.log(userPlans); + const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray(); + + for (const plan of userPlansFromDB) { + const percentage = await calculatePlanProgress(plan, assets, req.session.user._id); + await updatePlanProgressInDB(plan._id, percentage, plans); + } + + const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + + res.render('plans', { user: req.session.user, - plans: userPlans + plans: updatedUserPlans, + geoData: req.session.geoData }); } catch (err) { console.error("Error fetching plans:", err); @@ -89,29 +122,32 @@ module.exports = (middleware, users, plans, assets) => { }); router.get('/plans/:id', async (req, res) => { - + try { const planId = req.params.id; + let userAssets = await assets.find({ userId: new ObjectId(req.session.userId) }).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) }); + const plan = await plans.findOne({ userId: new ObjectId(req.session.userId), _id: new ObjectId(planId) }); if (!plan) { - console.log(`Plan not found with ID: ${planId} for user: ${req.session.user.email}`); + console.log(`Plan not found with ID: ${planId} for user: ${req.session.userId}`); req.session.errMessage = "Plan not found or you do not have permission to view it."; return res.status(status.NotFound).redirect('/plans'); } - // console.log("Found plan:", plan); - res.render('planDetail', { + + res.render('planDetail', { user: req.session.user, - plan: plan + plan: plan, + geoData: req.session.geoData, + assets: userAssets, + suggestions: await suggestions.generateSuggestions(), }); - + } catch (err) { console.error("Error fetching plan:", err); req.session.errMessage = "Could not load your plan. Please try again."; @@ -120,13 +156,17 @@ module.exports = (middleware, users, plans, assets) => { }); router.get('/newPlan', (req, res) => { - if(!req.session.user.financialData){ + 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 }); + req.session.errMessage = ""; + res.render('newPlan', { + user: req.session.user, + errMessage: errMessage, + geoData: req.session.geoData + }); }); router.post('/newPlan', async (req, res) => { @@ -138,61 +178,79 @@ module.exports = (middleware, users, plans, assets) => { retirementLiabilities: joi.number().min(0).required(), }); - const validationOptions = { convert: true, abortEarly: false }; + const validationOptions = { convert: true, abortEarly: false }; const { error, value } = planSchema.validate(req.body, validationOptions); - if (error) { + 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; + 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), + userId: new ObjectId(req.session.userId), name: value.name, retirementAge: value.retirementAge, retirementExpenses: value.retirementExpenses, retirementAssets: value.retirementAssets, retirementLiabilities: value.retirementLiabilities, - progress: "0%" + progress: "0" }; - try{ - await plans.insertOne({userId: new ObjectId(req.session.user._id), ...newPlan}); - req.session.errMessage = ""; - res.redirect('/plans'); + try { + await plans.insertOne({ userId: new ObjectId(req.session.userId), ...newPlan }); + req.session.errMessage = ""; + res.redirect('/plans'); } - catch(err){ + 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.post('/fact', async (req, res) => { + const factInput = req.body.fact; + const fact = await suggestions.generateFact(factInput); + return res.status(status.Ok).json({ fact }); + }); + router.get('/more', (req, res) => { - res.render('more', { user: req.session.user }); + 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 }); + 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 }); + 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 }); + req.session.errMessage = ""; + res.render('questionnaire', { + user: req.session.user, + errMessage: errMessage, + geoData: req.session.geoData + }); }); router.post('/questionnaire', (req, res) => { - // console.log("Questionnaire POST body:", req.body); - const questionnaireSchema = joi.object({ dob: joi.date().required(), education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(), @@ -202,22 +260,24 @@ module.exports = (middleware, users, plans, assets) => { assets: joi.number().min(0).required(), liabilities: joi.number().min(0).required(), }); - - const validationOptions = { convert: true, abortEarly: false }; + + const validationOptions = { convert: true, abortEarly: false }; const { error, value } = questionnaireSchema.validate(req.body, validationOptions); - - if (error) { + + let referrer = req.get('Referrer') || "/home"; + 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; + req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', '); + let redirect = referrer.includes("?profile") ? "/questionnaire?profile" : "/questionnaire"; + res.status(status.BadRequest).redirect(redirect); + return; } - - users.updateOne( - { _id: new ObjectId(req.session.user._id) }, - { - $set: { - financialData: true, + + users.updateOne( + { _id: new ObjectId(req.session.userId) }, + { + $set: { + financialData: true, dob: value.dob, education: value.education, maritalStatus: value.maritalStatus, @@ -225,36 +285,56 @@ module.exports = (middleware, users, plans, assets) => { expenses: value.expenses, assets: value.assets, liabilities: value.liabilities, - } + } } - ).then((result) => { + ).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"); + console.log(`User not found during questionnaire update: ${req.session.userId}`); + 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}`); + console.log(`User questionnaire data unchanged (already up-to-date): ${req.session.userId}`); } - - req.session.user.financialData = true; - req.session.errMessage = ""; - res.status(status.Ok).redirect("/home"); - - }).catch(err => { + + req.session.errMessage = ""; + req.session.user = null; // set user to null so middleware updates user + req.session.save((err) => { + if (err) { + console.error("Failed to save session: ", err); + + return req.session.destroy((err) => { + req.session.errMessage = "Failed to save session, please login again."; + + if (err) { + console.error("Failed to destroy session: ", err); + } + + res.status(status.InternalServerError); + return res.redirect("/login"); + }); + } + + let redirect = referrer.includes("?profile") ? "/profile" : + referrer != "/home" ? "/plans" : referrer; + return res.status(status.Ok).redirect(redirect); + }); + + }).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"); + let redirect = referrer.includes("?profile") ? "/questionnaire?profile" : "/questionnaire"; + res.status(status.InternalServerError).redirect(redirect); }); }); router.post("/updateAccount", async (req, res) => { const accountSchema = joi.object({ email: joi.string().email(), - name: joi.string().alphanum().max(20), - password: joi.string().max(20).min(8), - repassword: joi.string().max(20).min(8), + 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); @@ -266,6 +346,7 @@ module.exports = (middleware, users, plans, assets) => { } let update = { + email: req.body.email, name: req.body.name, }; @@ -279,29 +360,121 @@ module.exports = (middleware, users, plans, assets) => { } users.updateOne( - { email: req.session.email }, + { _id: new ObjectId(req.session.userId) }, { $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"); + console.log(`User not found during account update: ${req.session.userId}`); + 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}`); + console.log(`User account data unchanged (already up-to-date): ${req.session.userId}`); } - - req.session.errMessage = ""; - return res.status(status.Ok).redirect("/profile"); - }).catch(err => { + + req.session.errMessage = ""; + req.session.user = null; // set user to null so middleware updates user + req.session.save((err) => { + if (err) { + console.error("Failed to save session: ", err); + + return req.session.destroy((err) => { + req.session.errMessage = "Failed to save session, please login again."; + + if (err) { + console.error("Failed to destroy session: ", err); + } + + res.status(status.InternalServerError); + return res.redirect("/login"); + }); + } + + 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"); + return res.status(status.InternalServerError).redirect("/profile"); }); }); - router.post("/createAsset", async (req, res) => { + router.post("/updatePersonal", (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("Personal info validation error:", error.details); + req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', '); + res.status(status.BadRequest).redirect("/profile"); + return; + } + + users.updateOne( + { _id: new ObjectId(req.session.userId) }, + { + $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 personal info update: ${req.session.userId}`); + 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 personal info unchanged (already up-to-date): ${req.session.userId}`); + } + + req.session.errMessage = ""; + req.session.user = null; // set user to null so middleware updates user + req.session.save((err) => { + if (err) { + console.error("Failed to save session: ", err); + + return req.session.destroy((err) => { + req.session.errMessage = "Failed to save session, please login again."; + + if (err) { + console.error("Failed to destroy session: ", err); + } + + res.status(status.InternalServerError); + return res.redirect("/login"); + }); + } + + return res.status(status.Ok).redirect("/profile"); + }); + + }).catch(err => { + console.error("Error updating personal info in database:", err); + req.session.errMessage = "An error occurred while saving your information. Please try again."; + res.status(status.InternalServerError).redirect("/profile"); + }); + }); + + router.post("/createAsset", (req, res) => { // Create asset, each asset has different data structure based on type const type = req.body.type; const assetSchema = getAssetSchema(type); @@ -321,7 +494,7 @@ module.exports = (middleware, users, plans, assets) => { } let newAsset = { - userId: new ObjectId(req.session.user._id), + userId: new ObjectId(req.session.userId), ...req.body, updatedAt: new Date(), }; @@ -333,6 +506,7 @@ module.exports = (middleware, users, plans, assets) => { 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) { @@ -342,11 +516,11 @@ module.exports = (middleware, users, plans, assets) => { } }); - req.session.errMessage = ""; + req.session.errMessage = ""; return res.status(status.Ok).redirect("/assets"); }); - router.post("/updateAsset", async (req, res) => { + router.post("/updateAsset", (req, res) => { const type = req.body.type; const assetSchema = getAssetSchema(type); @@ -355,18 +529,18 @@ module.exports = (middleware, users, plans, assets) => { 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); + res.status(status.BadRequest); return res.redirect("/assets"); } - if (req.body.userId != req.session.user._id) { + if (req.body.userId != req.session.userId) { req.session.errMessage = "Cannot change asset owner", - res.status(status.BadRequest); + res.status(status.BadRequest); return res.redirect("/assets"); } @@ -378,12 +552,13 @@ module.exports = (middleware, users, plans, assets) => { update.name = `${update.ticker} Stock`; } update.value = parseFloat(update.value); - delete update.id - + delete update.id; + delete update.userId; + assets.updateOne( { "_id": new ObjectId(req.body.id) }, { $set: update }, - ).then((result) => { + ).then((result) => { if (result.matchedCount === 0) { console.log(`Asset not found: ${req.body.id}`); req.session.errMessage = "Unable to update asset"; @@ -392,14 +567,14 @@ module.exports = (middleware, users, plans, 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) => { + + 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"); + return res.status(status.InternalServerError).redirect("/assets"); }); }); @@ -408,7 +583,7 @@ module.exports = (middleware, users, plans, assets) => { assets.deleteOne( { "_id": id } - ).then((result) => { + ).then((result) => { if (result.deletedCount === 0) { console.error(`Asset not found: ${req.body.id}`); req.session.errMessage = "Unable to delete asset. Please try again."; @@ -420,11 +595,11 @@ module.exports = (middleware, users, plans, assets) => { 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) => { + + 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"); @@ -433,20 +608,20 @@ module.exports = (middleware, users, plans, 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) => { + assets.deleteMany({ userId: new ObjectId(req.session.userId) }).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) => { + plans.deleteMany({ userId: new ObjectId(req.session.userId) }).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) }, + { _id: new ObjectId(req.session.userId) }, ).then((result) => { if (result.deletedCount === 0) { console.error(`User not found: ${req.body.id}`); @@ -470,5 +645,30 @@ module.exports = (middleware, users, plans, assets) => { }); }); + 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); + + if (!results.exRates) { + req.session.geoData = { + message: "error" + } + } else { + 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/suggestions.js b/src/util/suggestions.js new file mode 100644 index 0000000..2bfb957 --- /dev/null +++ b/src/util/suggestions.js @@ -0,0 +1,24 @@ +const { GoogleGenAI } = require("@google/genai"); + +const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY }); + +async function generateFact(factInput) { + const response = await ai.models.generateContent({ + model: "gemini-2.0-flash", + contents: `Generate a concise investment fact about "${factInput}". If "${factInput}" is not directly investment-related, provide a general, useful investment fact instead. Deliver only the fact itself, with no extra text or explanation.`, + }); + return response.text; +} + +async function generateSuggestions() { + const response = await ai.models.generateContent({ + model: "gemini-2.0-flash", + contents: "Return a mock investment suggestion 3-5 lines only", + }); + return response.text; +} + +module.exports = { + generateFact, + generateSuggestions +}; diff --git a/src/views/assets.ejs b/src/views/assets.ejs index 40ad394..586160d 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -2,133 +2,7 @@ <%- include("./partials/header") %>
- - - -
-

Create New Asset

-
- -
-
- -
- -
- - - -
-
- - -
- - - - - - - - - - - - - - -
- - -
-
- - - - - - -
+ <%- include("./partials/createAsset", { icons: icons }) %> <% if (errMessage != "") { %>
<%= errMessage %>
@@ -186,11 +60,13 @@ <%- 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 index e4c9b36..86539d0 100644 --- a/src/views/dashboard.ejs +++ b/src/views/dashboard.ejs @@ -1,100 +1,116 @@ <%- include("./partials/fileHeader") %> - <%- include("./partials/header") %> + <%- include("./partials/header") %> -
-

Welcome: <%= user.name %>

- -
- - - Create new plan - -
- -
-
- -
- - - -
-

- 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 -

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- 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 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/footer") %> + <%- 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/login.ejs b/src/views/login.ejs index c8dbcbd..d35800f 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -25,7 +25,7 @@
- Forgot Password? + Forgot Password?
@@ -41,4 +41,4 @@
-<%- include("./partials/footer") %> \ No newline at end of file +<%- include("./partials/footer") %> diff --git a/src/views/more.ejs b/src/views/more.ejs index 59e3acf..934700e 100644 --- a/src/views/more.ejs +++ b/src/views/more.ejs @@ -6,5 +6,6 @@ <%- 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 index a05dd1a..5370290 100644 --- a/src/views/newPlan.ejs +++ b/src/views/newPlan.ejs @@ -37,4 +37,6 @@ +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/partials/fileHeader.ejs b/src/views/partials/fileHeader.ejs index 35b60ed..b15b69c 100644 --- a/src/views/partials/fileHeader.ejs +++ b/src/views/partials/fileHeader.ejs @@ -7,4 +7,4 @@ RCalculator - \ No newline at end of file + \ No newline at end of file diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index fabc132..9f828cf 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -1,11 +1,26 @@ +
+ + +
- + + + \ No newline at end of file diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs index 4dd5854..43747eb 100644 --- a/src/views/partials/header.ejs +++ b/src/views/partials/header.ejs @@ -1,10 +1,78 @@ +
-