diff --git a/.env.example b/.env.example index 255daf9..aaa7118 100755 --- a/.env.example +++ b/.env.example @@ -5,3 +5,4 @@ SECRET='123456789' GEOLOCATION_API='api_key' EMAIL_USER=mail@example.com EMAIL_PASS='password' +GOOGLE_API_KEY='api_key' diff --git a/app.js b/app.js index 56eb793..1e3c914 100644 --- a/app.js +++ b/app.js @@ -70,8 +70,18 @@ app.get('/login', (req, res) => { }); app.get('/aboutUs', (req, res) => { - res.render('aboutUs'); - return res.status(status.Ok); + if (!req.session.authenticated) { + res.render('aboutUs', { + geoData: undefined + }); + return res.status(status.Ok); + } else { + res.render('aboutUs', { + user: req.session.user, + geoData: req.session.geoData + }); + return res.status(status.Ok); + } }); app.get('/forgotPassword', (req, res) => { @@ -83,7 +93,6 @@ app.get('/forgotPassword', (req, res) => { return res.status(status.Ok); }); - // Reset with token given to user via email app.get('/reset/:token', async (req, res) => { const token = req.params.token; @@ -106,7 +115,6 @@ app.get('/reset/:token', async (req, res) => { }); }); - // 404 handler - keep the actual notFound route please // REALLY DONT DELETE THIS app.get('/notFound', (req, res) => { diff --git a/package-lock.json b/package-lock.json index 83cff57..cea4e81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@google/genai": "^0.14.0", "bcrypt": "^5.1.1", + "check-password-strength": "^3.0.0", "connect-mongo": "^5.1.0", "crypto": "^1.0.1", "dotenv": "^16.5.0", @@ -18,12 +20,28 @@ "express-session": "^1.18.1", "joi": "^17.13.3", "mongodb": "^6.16.0", - "nodemailer": "^7.0.3" + "nodemailer": "^7.0.3", + "sweetalert2": "^11.6.13" }, "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", @@ -89,6 +107,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", @@ -104,6 +131,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", @@ -211,12 +247,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", @@ -231,6 +301,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", @@ -302,6 +380,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", @@ -356,6 +447,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-password-strength": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-3.0.0.tgz", + "integrity": "sha512-XIBvWpb7/RI2DO05tMixE4WbFFvEC7ls/jr1VSrWgXvlCmdiH2fsdkixG0cNGs1uIXwoSqJ75T3s9H/TlJV6Cw==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -417,6 +517,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", @@ -508,6 +619,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", @@ -558,6 +677,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", @@ -594,6 +721,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", @@ -624,12 +759,38 @@ "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", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -721,6 +882,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", @@ -781,6 +974,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", @@ -874,6 +1119,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", @@ -911,6 +1221,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", @@ -945,6 +1269,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", @@ -957,6 +1318,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", @@ -978,6 +1351,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", @@ -1131,6 +1518,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", @@ -1162,6 +1560,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", @@ -1615,6 +2040,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", @@ -1641,6 +2071,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", @@ -1648,6 +2083,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", @@ -2007,6 +2451,16 @@ "node": ">=8" } }, + "node_modules/sweetalert2": { + "version": "11.6.13", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.6.13.tgz", + "integrity": "sha512-n5yVF0FNx1lm4XzpPyb1HIaiptzODfVyeCzmB809tpK+1bPdoKoevKOxYjrtId75DV7xuIp4r6cjn8xUAB8dPQ==", + "license": "MIT", + "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", @@ -2024,6 +2478,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", @@ -2101,6 +2566,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", @@ -2116,6 +2587,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", @@ -2162,11 +2645,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 1071779..0d1c5c1 100755 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ }, "homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme", "dependencies": { + "@google/genai": "^0.14.0", "bcrypt": "^5.1.1", + "check-password-strength": "^3.0.0", "connect-mongo": "^5.1.0", "crypto": "^1.0.1", "dotenv": "^16.5.0", @@ -27,7 +29,8 @@ "express-session": "^1.18.1", "joi": "^17.13.3", "mongodb": "^6.16.0", - "nodemailer": "^7.0.3" + "nodemailer": "^7.0.3", + "sweetalert2": "^11.6.13" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/src/auth/authentication.js b/src/auth/authentication.js index d65e9c6..f70635b 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -1,5 +1,5 @@ +const { passwordStrength } = require("check-password-strength"); const status = require("../util/statuses"); -const session = require("express-session"); const bcrypt = require('bcrypt'); const joi = require("joi"); const salt = 12; @@ -12,10 +12,18 @@ 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({ @@ -46,10 +54,19 @@ 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"); + }); }); }); @@ -82,6 +99,14 @@ module.exports = (users) => { return res.redirect("/signup"); } + let strength = passwordStrength(req.body.password); + + if (strength.id < 2) { + req.session.errMessage = `Password ${strength.value}`; + res.status(status.BadRequest); + return res.redirect("/signup"); + } + let hashedPassword = await bcrypt.hashSync(req.body.password, salt); users.insertOne({ @@ -97,11 +122,20 @@ module.exports = (users) => { } 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 index 698a500..424c633 100644 --- a/src/auth/forgotPass.js +++ b/src/auth/forgotPass.js @@ -4,6 +4,7 @@ const joi = require('joi'); const nodeMail = require('nodemailer'); const bcrypt = require('bcrypt'); require('dotenv').config(); + const PORT = process.env.PORT; const transporter = nodeMail.createTransport({ @@ -13,7 +14,7 @@ const transporter = nodeMail.createTransport({ pass: process.env.EMAIL_PASS, } }); -// users info + module.exports = (users) => { const router = express.Router(); @@ -65,6 +66,7 @@ module.exports = (users) => { } }); + //Reset link verifies information from user and checks token router.post('/resetLink', async (req, res) => { const { token, password, confirmPassword, } = req.body; const passwordSchema = joi.object({ @@ -73,7 +75,7 @@ module.exports = (users) => { }); const valid = passwordSchema.validate({ password, confirmPassword }); if (valid.error) { - console.log("houston we have a problem"); + console.log("houston we have a problem"); // nice req.session.error = 'Invalid input'; res.status(status.BadRequest); return res.redirect(`/reset/${token}`); diff --git a/src/auth/middleware.js b/src/auth/middleware.js index 5c24807..05f5d5e 100644 --- a/src/auth/middleware.js +++ b/src/auth/middleware.js @@ -1,5 +1,6 @@ const status = require("../util/statuses"); const session = require("express-session"); +const ObjectId = require("mongodb").ObjectId; // Get all names of user routes let userRouter = require("../router/user")((req, res, next) => next(), null, null, null); @@ -18,26 +19,52 @@ const createMiddleware = (users) => { 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/dollarFormat.js b/src/public/scripts/dollarFormat.js new file mode 100644 index 0000000..a22f2c5 --- /dev/null +++ b/src/public/scripts/dollarFormat.js @@ -0,0 +1,20 @@ +/** + * Function takes in a number and formats it to currency + * @param {integer} + * @returns nuber formatted in currency + */ + +document.addEventListener("DOMContentLoaded", () => { + const number = document.querySelectorAll(".planGoal"); + number.forEach(value => { + num = parseFloat(value.textContent); + if (num >= 999999) { + value.textContent = "$" + (num /= 1000000) + "M"; + } else if (num > 999) { + value.textContent = "$" + (num /= 1000) + "k"; + } else { + return "$" + num; + } + }); +}); + diff --git a/src/public/scripts/footer.js b/src/public/scripts/footer.js new file mode 100644 index 0000000..e63f58a --- /dev/null +++ b/src/public/scripts/footer.js @@ -0,0 +1,70 @@ +const factButton = document.getElementById("factButton"); +const factMenu = document.getElementById("factMenu"); +const factInput = document.getElementById("factInput"); +const factSubmitButton = document.getElementById("factSubmitButton"); + +var path = window.location.pathname; +var page = path.split("/").pop(); + +if(page !== "login" && page !== "signup" && page !== "forgotPassword" && page !== "") { + factButton.classList.remove("hidden"); +} + +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'); + }); +}); + +document.addEventListener("DOMContentLoaded", () => { + const drawer = document.getElementById("drawer-navigation"); + const toggle = document.getElementById("drawer-toggle"); + + function openDrawer() { + drawer.classList.remove("translate-x-full"); + + // Add backdrop + const backdrop = document.createElement("div"); + backdrop.className = "fixed inset-0 bg-gray-900/50 z-30 drawer-backdrop"; + document.body.appendChild(backdrop); + + // Optional: prevent scroll + document.body.classList.add("overflow-hidden"); + + backdrop.addEventListener("click", closeDrawer); + } + + function closeDrawer() { + drawer.classList.add("translate-x-full"); + + const backdrop = document.querySelector(".drawer-backdrop"); + if (backdrop) backdrop.remove(); + + document.body.classList.remove("overflow-hidden"); + } + + toggle.addEventListener("click", openDrawer); + + // Also close on X button if needed + const closeBtn = drawer.querySelector("[data-drawer-hide]"); + if (closeBtn) { + closeBtn.addEventListener("click", closeDrawer); + } +}); \ No newline at end of file diff --git a/src/public/scripts/geolocation.js b/src/public/scripts/geolocation.js index 7fce450..94c9c66 100644 --- a/src/public/scripts/geolocation.js +++ b/src/public/scripts/geolocation.js @@ -5,38 +5,47 @@ function getLocation() { } function update(data) { - document.getElementById("loading").style = "display: none"; - document.getElementById("currencyExchange").style = "display: flex"; - - document.getElementById("yourFlag").src = `/static/svgs/flags/${data.data.country}.svg` - document.getElementById("yourFlag").alt = data.data.country; - document.getElementById("flagTag").innerHTML = `(${data.data.country})`; - - document.getElementById("exchangeFlag").src = `/static/svgs/flags/USD.svg`; - document.getElementById("exchangeFlag").alt = "USD"; - document.getElementById("exFlagTag").innerHTML = "(USD)"; + if (data.data.message != "error") { + document.getElementById("loading").style = "display: none"; + document.getElementById("currencyExchange").style = "display: flex"; - 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); - }); + 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"; + } } function switchButton(clickedButton) { @@ -60,10 +69,16 @@ async function getLatestExchange(position) { 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/router/user.js b/src/router/user.js index e4511ad..7a25e91 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -1,5 +1,6 @@ const getRates = require("../util/exchangeRate"); -const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations"); +const { calculateProgress, updatePlanProgressInDB } = require("../util/calculations"); +const suggestions = require("../util/suggestions"); const status = require("../util/statuses"); const ObjectId = require('mongodb').ObjectId; const session = require("express-session"); @@ -55,6 +56,12 @@ const getAssetSchema = (type) => { return assetSchema; } +/** + * Function takes in a number and formats it to currency + * @param {integer} + * @returns nuber formatted in currency + */ + /** * @param {function} middleware handler * @param {MongoClient.collection} users db collection @@ -71,6 +78,7 @@ module.exports = (middleware, users, plans, assets) => { router.use(middleware); router.get('/home', async (req, res) => { + let user = req.session.user._id; // if no session with geoData if (!req.session.geoData) { req.session.geoData = { @@ -78,14 +86,26 @@ module.exports = (middleware, users, plans, assets) => { toCurrencyRates: [], }; } + const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray(); - res.render('dashboard', { user: req.user, geoData: req.session.geoData }); + for (const plan of userPlansFromDB) { + const progress = await calculateProgress(plan, assets, users, req.session.user._id); + await updatePlanProgressInDB(plan._id, progress.percentage, plans); + } + + const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + + res.render('dashboard', { + user: req.session.user, + geoData: req.session.geoData, + plans: updatedUserPlans, + }); return res.status(status.Ok); }); router.get('/assets', async (req, res) => { - let userAssets = await assets.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + let userAssets = await assets.find({ userId: new ObjectId(req.session.userId) }).toArray(); res.render('assets', { user: req.session.user, errMessage: req.session.errMessage, @@ -98,20 +118,19 @@ module.exports = (middleware, users, plans, assets) => { router.get('/plans', async (req, res) => { try { - const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray(); - // Use a for...of loop for proper async/await behavior in series for updates for (const plan of userPlansFromDB) { - const percentage = await calculatePlanProgress(plan, assets, req.session.user._id); - await updatePlanProgressInDB(plan._id, percentage, plans); // Pass the 'plans' collection + const progress = await calculateProgress(plan, assets, users, req.session.user._id); + await updatePlanProgressInDB(plan._id, progress.percentage, plans); } - // Re-fetch plans to get updated progress for rendering const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + res.render('plans', { user: req.session.user, - plans: updatedUserPlans, // Send the most up-to-date plans + plans: updatedUserPlans, geoData: req.session.geoData }); } catch (err) { @@ -125,34 +144,30 @@ module.exports = (middleware, users, plans, assets) => { try { const planId = req.params.id; - let userAssets = await assets.find({ userId: new ObjectId(req.session.user._id) }).toArray(); - + 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'); } + const progress = await calculateProgress(plan, assets, users, req.session.userId); - // The plan.progress should be up-to-date from the database as it was updated in the /plans route - // or when assets/plans are modified. If an immediate recalculation for this specific view is absolutely needed, - // (e.g., if assets were modified without an immediate plan progress update elsewhere), - // you could do it here: - // const currentProgress = await calculatePlanProgress(plan, assets, req.session.user._id); - // plan.progress = currentProgress; // This would only update the 'plan' object for this render, not in DB res.render('planDetail', { user: req.session.user, - plan: plan, // This plan object will have the progress from the database + plan: plan, geoData: req.session.geoData, assets: userAssets, + progress: progress, + suggestions: await suggestions.generateSuggestions(), }); } catch (err) { @@ -195,7 +210,7 @@ module.exports = (middleware, users, plans, assets) => { 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, @@ -205,7 +220,7 @@ module.exports = (middleware, users, plans, assets) => { }; try { - await plans.insertOne({ userId: new ObjectId(req.session.user._id), ...newPlan }); + await plans.insertOne({ userId: new ObjectId(req.session.userId), ...newPlan }); req.session.errMessage = ""; res.redirect('/plans'); } @@ -216,6 +231,12 @@ module.exports = (middleware, users, plans, assets) => { } }); + 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, @@ -265,15 +286,17 @@ module.exports = (middleware, users, plans, assets) => { const validationOptions = { convert: true, abortEarly: false }; const { error, value } = questionnaireSchema.validate(req.body, validationOptions); + 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"); + let redirect = referrer.includes("?profile") ? "/questionnaire?profile" : "/questionnaire"; + res.status(status.BadRequest).redirect(redirect); return; } users.updateOne( - { _id: new ObjectId(req.session.user._id) }, + { _id: new ObjectId(req.session.userId) }, { $set: { financialData: true, @@ -288,29 +311,43 @@ module.exports = (middleware, users, plans, assets) => { } ).then((result) => { if (result.matchedCount === 0) { - console.log(`User not found during questionnaire update: ${req.session.user.email}`); + 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 = ""; - - req.session.save(err => { + req.session.user = null; // set user to null so middleware updates user + req.session.save((err) => { if (err) { - res.status(status.InternalServerError).redirect("/plans"); + 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"); + }); } - res.status(status.Ok).redirect("/plans"); + + 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); }); }); @@ -331,6 +368,7 @@ module.exports = (middleware, users, plans, assets) => { } let update = { + email: req.body.email, name: req.body.name, }; @@ -344,21 +382,39 @@ 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}`); + 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"); + 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."; @@ -366,7 +422,81 @@ module.exports = (middleware, users, plans, assets) => { }); }); - 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); @@ -386,7 +516,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(), }; @@ -412,7 +542,7 @@ module.exports = (middleware, users, plans, assets) => { 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); @@ -430,7 +560,7 @@ module.exports = (middleware, users, plans, assets) => { 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); return res.redirect("/assets"); @@ -500,20 +630,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}`); @@ -544,11 +674,19 @@ module.exports = (middleware, users, plans, assets) => { country = data.results[0].formatted_address; let results = await getRates(country); - req.session.geoData = { - country: results.abbreviation, - toCurrencyRates: results.exRates, - geoData: req.session.geoData + + 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 }); diff --git a/src/util/calculations.js b/src/util/calculations.js index 25f5d33..19d3222 100644 --- a/src/util/calculations.js +++ b/src/util/calculations.js @@ -42,4 +42,72 @@ async function updatePlanProgressInDB(planId, percentage, plans) { } } -module.exports = { calculatePlanProgress, updatePlanProgressInDB }; \ No newline at end of file + +async function calculateProgress(plan, assets, users, userId) { + if (!plan || typeof plan !== 'object') { + console.error("Error with the plan"); + return; + } + if (!assets || typeof assets.find !== 'function') { + console.error("Error with the assets collection"); + return; + } + if (!users || typeof users.findOne !== 'function') { + console.error("Error with the users collection"); + return; + } + if (!userId) { + console.error("No user ID provided"); + return; + } + + try { + const userAssets = await assets.find({ userId: new ObjectId(userId) }).toArray(); + const totalUserAssetValue = userAssets.reduce((total, asset) => total + asset.value, 0); + const totalUserPlanValue = plan.retirementAssets; + + const userDoc = await users.findOne({ _id: new ObjectId(userId) }); + if (!userDoc || !userDoc.dob) { + console.error("User document or DOB not found for userId:", userId); + return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, percentage: NaN }; + } + const userDob = new Date(userDoc.dob); + + if (isNaN(userDob.getTime())) { + console.error("userDob is an invalid date. Aborting calculation."); + return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, percentage: NaN }; + } + + const today = new Date(); + const userUnalivedBy = new Date(userDob); + userUnalivedBy.setFullYear(userUnalivedBy.getFullYear() + 90); + const yearOfRetirement = userDob.getFullYear() + plan.retirementAge; + const monthsUntilRetirement = (yearOfRetirement - today.getFullYear()) * 12; + const yearsRetired = userUnalivedBy.getFullYear() - yearOfRetirement; + + const totalCostOfRetirement = ((plan.retirementExpenses + plan.retirementLiabilities) * 12) * yearsRetired; + + const monthlyInvestment = (totalUserPlanValue - totalUserAssetValue + totalCostOfRetirement) / monthsUntilRetirement; + + const percentageCalculated = (totalUserAssetValue / (totalUserPlanValue + totalCostOfRetirement)) * 100; + + const progress = {}; + + progress.monthlyInvestment = Math.round(monthlyInvestment); + progress.totalCostOfRetirement = totalCostOfRetirement; + progress.monthsUntilRetirement = monthsUntilRetirement; + progress.yearsRetired = yearsRetired; + progress.yearsUntilRetirement = (yearOfRetirement - today.getFullYear()); + progress.percentage = percentageCalculated; + + return progress; + + } catch (err) { + console.error("Error in calculateProgress:", err); + return; + } +} + + + +module.exports = { calculatePlanProgress, updatePlanProgressInDB, calculateProgress}; \ 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/aboutUs.ejs b/src/views/aboutUs.ejs index 32ded54..dc26bdb 100644 --- a/src/views/aboutUs.ejs +++ b/src/views/aboutUs.ejs @@ -1,19 +1,24 @@ <%- include("./partials/fileHeader") %> -<%- include("./partials/headerStart") %> -
-
+<% if (!geoData) { %> + <%- include("./partials/headerStart") %> +<% } else { %> + <%- include("./partials/header") %> +<% } %> + +
+
-

About RCalculator

-

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

+

About RCalculator

+

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

-

Our Mission

-

+

Our Mission

+

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

-
+

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

@@ -25,15 +30,23 @@

- + + <% if (!geoData) { %> + + <% } %>
+<% if (!geoData) { %> +<% } else { %> + <%- include("./partials/navBar") %> +<% } %> + <%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/assets.ejs b/src/views/assets.ejs index 764e6bd..5397a5f 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -1,157 +1,8 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/header") %> -
- - -
-

Create New Asset

-
- -
-
- -
- -
- - - -
-
- - -
- - - - -
- - -
- - - - - - - - - - - - - -
- - -
-
- - - - - - -
+
+ <%- include("./partials/createAsset", { icons: icons }) %> <% if (errMessage != "") { %>
<%= errMessage %>
@@ -196,183 +47,11 @@ <% } %> <% assets.forEach((asset) => { %> - -
- -
+ <%- include("./partials/assetPreview", { asset: asset }); %> - - -
- -
-
- -
- -

Are you sure you want to delete this item?

-
-
-
- -
-
-
-
- - -
-
-
-
-
-
+ <%- include("./partials/assetDelete", { asset: asset }); %> - - -
-
-
- -
-
-
- -
-
-
- -
- <% if (asset.type == "other") { %> -
- - -
- <% } else { %> - - <% } %> - -

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

-
- -
-
-
- -
- - - - - - <% if (asset.type != "stock") { %> - - - <% } else { %> - - - <% } %> - -
- - <% if (asset.type != "stock") { %> - - - <% } else { %> - - - - - - <% } %> - - <% if (asset.type == "other") { %> - - - <% } %> - - <% if (asset.type != "saving") { %> - - - <% } %> - -
- Last Modified: <%= asset.updatedAt %> -
- -
- -
-
-
+ <%- include("./partials/assetModify", { asset: asset }); %> <% }) %>
@@ -382,11 +61,12 @@ <%- include("./partials/navBar") %> <%- include("./partials/scriptLoader") %> -<%- include("./partials/footer") %> \ No newline at end of file +<%- include("./partials/footer") %> diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs index 86539d0..d306208 100644 --- a/src/views/dashboard.ejs +++ b/src/views/dashboard.ejs @@ -1,7 +1,7 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/header") %> -
+
- -
- - - - - -
-
- -
- - - -
-

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

-
-
-
-
-
-
-
+
+
+ <% plans.forEach(plan=> { %> + <%- include('./partials/dashboardBox.ejs', {thing: plan}) %> + <% }); %> +
+
+ + <%- include("./partials/navBar") %> <%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> diff --git a/src/views/forgotPass.ejs b/src/views/forgotPass.ejs index d2d749f..a7a656c 100644 --- a/src/views/forgotPass.ejs +++ b/src/views/forgotPass.ejs @@ -1,38 +1,42 @@ <%- include("./partials/fileHeader") %> +<%- include("./partials/headerStart") %> -
-
-
-
-

Reset Password

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

Reset link sent.

-
- <% } %> - +
+
+ +
+

Reset Password

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

Reset link sent.

+

Please check your email inbox (and spam folder).

+
+ <% } %> + + <% if (typeof error !== 'undefined' && error && error.length > 0) { %> +
<%= error %>
+ <% } else if (!reset) { %> +
<% /* Placeholder for layout consistency */ %> + <% } %> - <%- include("./partials/footer") %> <%- include("./partials/footer") %> + <% if (!reset) { %> +
+ +
+ <% } %> +
+ +
+
+ +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/landing.ejs b/src/views/landing.ejs index d7a24d1..afcbbd6 100644 --- a/src/views/landing.ejs +++ b/src/views/landing.ejs @@ -7,7 +7,7 @@

Plan now, live better.

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

- + Get started
+
+
-
-

Login

-
-
- +
+

Login

+
+
+
-
- +
+
-
<%= errMessage %>
-
-
- - + <% if (errMessage && errMessage.length > 0) { %> +
<%= errMessage %>
+ <% } else { %> +
<% /* Placeholder to maintain layout when no error */ %> + <% } %> +
-
+
+ class="border-2 border-indigo-600 bg-indigo-600 text-white py-2 w-full rounded-md hover:bg-indigo-700 hover:border-indigo-700 font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Login
-
- - Register +
+ Don't have an account? + Register
diff --git a/src/views/more.ejs b/src/views/more.ejs index 934700e..8e9020d 100644 --- a/src/views/more.ejs +++ b/src/views/more.ejs @@ -1,11 +1,54 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/header") %> -
- The More Page +
+
<%- include("./partials/navBar") %> <%- include("./partials/scriptLoader") %> -<%- include("./partials/footer") %> - \ No newline at end of file +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/newPlan.ejs b/src/views/newPlan.ejs index 5370290..d337639 100644 --- a/src/views/newPlan.ejs +++ b/src/views/newPlan.ejs @@ -1,8 +1,7 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/header") %> -
-

Welcome: <%= user.name %>

+

Retirement Plan

diff --git a/src/views/notFound.ejs b/src/views/notFound.ejs index 2e783bc..2b489b6 100644 --- a/src/views/notFound.ejs +++ b/src/views/notFound.ejs @@ -7,7 +7,7 @@

404 - Not found

It appears you stumbled accross a misleading page.

- + Go home
+ +
+ + + + +

Are you sure you want to delete this item?

+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ \ No newline at end of file diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs new file mode 100644 index 0000000..82059c8 --- /dev/null +++ b/src/views/partials/assetModify.ejs @@ -0,0 +1,119 @@ + + + +
+
+
+ +
+
+
+ +
+
+
+ +
+ <% if (asset.type == "other") { %> +
+ + +
+ <% } else { %> + + <% } %> + +

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

+
+ +
+
+
+ +
+ + + + + + <% if (asset.type != "stock") { %> + + + <% } else { %> + + + <% } %> + +
+ + <% if (asset.type != "stock") { %> + + + <% } else { %> + + + + + + <% } %> + + <% if (asset.type == "other") { %> + + + <% } %> + + <% if (asset.type != "saving") { %> + + + <% } %> + +
+ Last Modified: <%= asset.updatedAt %> +
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/views/partials/assetPreview.ejs b/src/views/partials/assetPreview.ejs new file mode 100644 index 0000000..4d8c588 --- /dev/null +++ b/src/views/partials/assetPreview.ejs @@ -0,0 +1,18 @@ + +
+ +
\ No newline at end of file diff --git a/src/views/partials/createAsset.ejs b/src/views/partials/createAsset.ejs new file mode 100644 index 0000000..b6624c7 --- /dev/null +++ b/src/views/partials/createAsset.ejs @@ -0,0 +1,150 @@ + + +
+

Create New Asset

+
+ +
+
+ +
+ +
+ + + +
+
+ + +
+ + + + +
+ + +
+ + + + + + + + + + + + + +
+ + +
+
+ + + + + + +
\ No newline at end of file diff --git a/src/views/partials/dashboardBox.ejs b/src/views/partials/dashboardBox.ejs new file mode 100644 index 0000000..6e9fc0c --- /dev/null +++ b/src/views/partials/dashboardBox.ejs @@ -0,0 +1,35 @@ +
diff --git a/src/views/partials/fileHeader.ejs b/src/views/partials/fileHeader.ejs index b15b69c..b3e6c69 100644 --- a/src/views/partials/fileHeader.ejs +++ b/src/views/partials/fileHeader.ejs @@ -6,5 +6,16 @@ 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 3a39799..b450ef8 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -1,13 +1,28 @@ - -
-
- - +
+ +
+ + + + + diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs index 90410ca..d118c09 100644 --- a/src/views/partials/header.ejs +++ b/src/views/partials/header.ejs @@ -1,66 +1,68 @@
-
\ No newline at end of file diff --git a/src/views/partials/headerStart.ejs b/src/views/partials/headerStart.ejs index 9b215d4..c716413 100644 --- a/src/views/partials/headerStart.ejs +++ b/src/views/partials/headerStart.ejs @@ -1,12 +1,12 @@