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..741673f 100644
--- a/app.js
+++ b/app.js
@@ -83,7 +83,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 +105,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..be5d1bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"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",
@@ -18,12 +19,28 @@
"express-session": "^1.18.1",
"joi": "^17.13.3",
"mongodb": "^6.16.0",
- "nodemailer": "^7.0.3"
+ "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",
@@ -89,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",
@@ -104,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",
@@ -211,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",
@@ -231,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",
@@ -302,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",
@@ -417,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",
@@ -508,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",
@@ -558,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",
@@ -594,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",
@@ -624,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",
@@ -721,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",
@@ -781,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",
@@ -874,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",
@@ -911,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",
@@ -945,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",
@@ -957,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",
@@ -978,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",
@@ -1131,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",
@@ -1162,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",
@@ -1615,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",
@@ -1641,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",
@@ -1648,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",
@@ -2007,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",
@@ -2024,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",
@@ -2101,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",
@@ -2116,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",
@@ -2162,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 1071779..597241f 100755
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
},
"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",
@@ -27,7 +28,8 @@
"express-session": "^1.18.1",
"joi": "^17.13.3",
"mongodb": "^6.16.0",
- "nodemailer": "^7.0.3"
+ "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 d65e9c6..d32f0a8 100644
--- a/src/auth/authentication.js
+++ b/src/auth/authentication.js
@@ -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");
+ });
});
});
@@ -97,11 +114,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 85dd678..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();
@@ -74,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/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 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 4b67b48..e89db9f 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 suggestions = require("../util/suggestions");
const status = require("../util/statuses");
const ObjectId = require('mongodb').ObjectId;
const session = require("express-session");
@@ -94,7 +95,7 @@ module.exports = (middleware, users, plans, assets) => {
});
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,
@@ -107,20 +108,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
+ await updatePlanProgressInDB(plan._id, 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) {
@@ -134,34 +134,27 @@ 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');
}
- // 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,
+ suggestions: await suggestions.generateSuggestions(),
});
} catch (err) {
@@ -204,7 +197,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,
@@ -214,7 +207,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');
}
@@ -225,6 +218,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,
@@ -274,15 +273,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,
@@ -297,29 +298,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);
});
});
@@ -335,11 +350,12 @@ module.exports = (middleware, users, plans, assets) => {
if (valid.err) {
req.session.errMessage = "Invalid input",
- res.status(status.BadRequest);
+ res.status(status.BadRequest);
return res.redirect("/profile");
}
let update = {
+ email: req.body.email,
name: req.body.name,
};
@@ -353,21 +369,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.";
@@ -375,7 +409,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);
@@ -390,12 +498,12 @@ module.exports = (middleware, users, plans, assets) => {
if (valid.err) {
req.session.errMessage = "Invalid input",
- res.status(status.BadRequest);
+ res.status(status.BadRequest);
return res.redirect("/assets");
}
let newAsset = {
- userId: new ObjectId(req.session.user._id),
+ userId: new ObjectId(req.session.userId),
...req.body,
updatedAt: new Date(),
};
@@ -421,7 +529,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);
@@ -439,7 +547,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");
@@ -509,20 +617,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}`);
@@ -553,11 +661,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/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 764e6bd..26129a6 100644
--- a/src/views/assets.ejs
+++ b/src/views/assets.ejs
@@ -2,156 +2,7 @@
<%- include("./partials/header") %>