Compare commits

...
Author SHA1 Message Date
Nícolas Agostini c41f38ce6b Merge pull request #27 from JoaquinPar/feature/ai-challenge2
Feature/ai challenge2
2025-05-14 11:24:40 -07:00
Nícolas Agostini 5771f1372f Merge branch 'dev' into feature/ai-challenge2 2025-05-14 11:24:31 -07:00
nicoagostini 73d61d76de feature/AI feature updated to accept user input 2025-05-14 11:22:28 -07:00
nicoagostini 50e2d95fda feature/AI feature updated to accept user input 2025-05-14 11:21:10 -07:00
Joaquin 370d5d3292 merge dev into feature/challenge1_error_message 2025-05-14 09:41:39 -07:00
Joaquin c12bbe8336 removed debugging code 2025-05-14 09:40:06 -07:00
Joaquin 0d59c60a28 merge dev into feature/challenge1_error_message 2025-05-14 09:39:12 -07:00
Joaquin 5e2ac94212 fully implemented error handling for challenge 1 2025-05-14 09:37:09 -07:00
SowinskiBraeden f5bc2de24a Merge pull request #26 from JoaquinPar/fix/session_issues
fix/login signup logout session bugs
2025-05-14 09:36:38 -07:00
SowinskiBraeden 3771288123 fix/login+signup+logout session bugs 2025-05-14 09:34:32 -07:00
Nícolas Agostini ccae6a51ff Merge pull request #25 from JoaquinPar/feature/ai-challenge2
Feature/ai challenge2
2025-05-13 14:19:17 -07:00
nicoagostini 206b31e785 feature/ Surprise challenge #2 - AI pun button added and basic suggestion integrated in the plans 2025-05-13 14:18:21 -07:00
Joaquin 8166245ab1 feature/basic functionality added 2025-05-13 13:17:18 -07:00
Joaquin Paredes 79f0caa3c1 Merge pull request #23 from JoaquinPar/dev
End of Sprint 2
2025-05-10 16:35:08 -07:00
Nícolas Agostini 4f73959cfb Merge pull request #22 from JoaquinPar/fix/16-hovering-hides-progress
Progress bar background changed to black to avoid disappearing
2025-05-09 15:54:45 -07:00
nicoagostini 6444d7afa7 Progress bar background changed to black to avoid disappearing 2025-05-09 15:53:40 -07:00
Nícolas Agostini daa984a48f Merge pull request #21 from JoaquinPar/fix/questionnaireFix
Issue 19: Questionnaire more than once, fixed
2025-05-09 14:04:31 -07:00
nicoagostini 383bd15263 Issue 19: Questionnaire more than once, fixed 2025-05-09 14:03:35 -07:00
knighthawk4227 1e44e8ddfb moving resetLink post to forgotpass.js 2025-05-09 11:25:36 -07:00
knighthawk4227 cc875b4e22 Merge branch 'dev' of github.com:JoaquinPar/2800-202510-BBY14 into dev 2025-05-09 11:24:03 -07:00
knighthawk4227 f3dbbebb43 moving resetLink post to forgotpass.js 2025-05-09 11:18:25 -07:00
Joaquin 4ee885ac57 fix/reorganized scripts called in ejs files 2025-05-09 11:06:13 -07:00
knighthawk4227 f3b121fc71 Merge branch 'feature/forgotPass' into dev 2025-05-09 11:03:02 -07:00
knighthawk4227 07824e6355 merge cnflicts with dev 2025-05-09 11:02:31 -07:00
Joaquin 22850ab2a2 fix/removed redundant code and fixed UI challenge 1 problem 2025-05-09 10:55:02 -07:00
SowinskiBraeden 69fd776c0f fix/enhanced user input validation 2025-05-09 10:29:20 -07:00
knighthawk4227 671b67be9d Merge branch 'dev' into feature/forgotPass 2025-05-09 10:27:33 -07:00
knighthawk4227 99b36d3370 fixing conflict with dashboard.ejs 2025-05-09 10:26:18 -07:00
SowinskiBraeden 1f742d41c3 fix/signup using existing email 2025-05-09 10:23:34 -07:00
SowinskiBraeden 18bee21941 Merge branch 'dev' of github.com:JoaquinPar/2800-202510-BBY14 into dev 2025-05-09 10:07:16 -07:00
knighthawk4227 1fcd12c6da fixing merge conflicts 2025-05-09 10:04:24 -07:00
SowinskiBraeden 0a9c176274 fix/default asset icon creation 2025-05-09 10:00:51 -07:00
knighthawk4227 1587f363f7 fixing reset link session 2025-05-09 09:50:38 -07:00
SowinskiBraeden 540e191d27 refactor/remove debug statements 2025-05-09 09:49:14 -07:00
SowinskiBraeden 1e01e022bf Merge pull request #14 from JoaquinPar/feature/asset-select-icon
update+fix/asset icons + load geolocation scripts
2025-05-08 23:32:40 -07:00
SowinskiBraeden 73a510252b update+fix/asset icons + load geolocation scripts 2025-05-08 23:30:13 -07:00
SowinskiBraeden 42ea4ed8bc fix/issue #13 2025-05-08 21:40:39 -07:00
Nícolas Agostini 1ddb6a9900 Merge pull request #12 from JoaquinPar/feature/progressBar
Progress bar calculations added, displaying and updating the database
2025-05-08 20:10:37 -07:00
knighthawk4227 c64da1de46 fixing conflicts and merging dev 2025-05-08 10:44:38 -07:00
knighthawk4227 7a5d2e7490 fixing conflicts merging dev into this 2025-05-08 10:40:31 -07:00
knighthawk4227 fc39bcf693 nodemailer dependencies 2025-05-08 10:32:10 -07:00
knighthawk4227 29d682c4e8 fixing joi validation 2025-05-07 19:26:00 -07:00
knighthawk4227 15dd4e0d20 forgot password validatation using joi 2025-05-07 19:09:25 -07:00
knighthawk4227 cb59e86d8a fixing merge conflicts and forgot password functionality 2025-05-07 17:44:06 -07:00
knighthawk4227 088f9ea55a reset pass logic 2025-05-07 13:03:26 -07:00
knighthawk4227 617fc8d877 indentation of dashboard 2025-05-07 10:49:08 -07:00
knighthawk4227 f0b1089e15 routes for forgotPass 2025-05-07 00:52:01 -07:00
knighthawk4227 a40b6d514b forgot password js 2025-05-06 23:33:05 -07:00
Joaquin Paredes 5819b0d151 Merge pull request #1 from JoaquinPar/dev
Merging Dev into Main Sprint #0
2025-04-29 16:07:03 -07:00
43 changed files with 1338 additions and 275 deletions

No files matched your search

+1
View File
@@ -5,3 +5,4 @@ SECRET='123456789'
GEOLOCATION_API='api_key' GEOLOCATION_API='api_key'
EMAIL_USER=mail@example.com EMAIL_USER=mail@example.com
EMAIL_PASS='password' EMAIL_PASS='password'
GOOGLE_API_KEY='api_key'
+3
View File
@@ -1,6 +1,9 @@
# Environment variables # Environment variables
.env .env
#vscode files
.vscode
# Node modules # Node modules
node_modules/ node_modules/
+37 -3
View File
@@ -3,6 +3,7 @@ const MongoStore = require("connect-mongo");
const session = require("express-session"); const session = require("express-session");
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const bcrypt = require('bcrypt');
const joi = require('joi'); const joi = require('joi');
require('dotenv').config(); require('dotenv').config();
@@ -11,7 +12,7 @@ const port = process.env.PORT || 3000;
const mongoURI = process.env.MONGO_URI; const mongoURI = process.env.MONGO_URI;
const database = process.env.DATABASE; // Database name const database = process.env.DATABASE; // Database name
const secret = process.env.SECRET || "123-secret-xyz"; const secret = process.env.SECRET || "123-secret-xyz";
/*** Sessions ***/ /*** Sessions ***/
app.use(session({ app.use(session({
@@ -39,9 +40,9 @@ async function initDatabase() {
const db = await connectMongo(mongoURI, database); const db = await connectMongo(mongoURI, database);
// For any collection, init here // For any collection, init here
users = await getCollection(db, "users"); users = await getCollection(db, "users");
assets = await getCollection(db, "assets"); assets = await getCollection(db, "assets");
plans = await getCollection(db, "plans"); plans = await getCollection(db, "plans");
} }
/*** ROUTINGS ***/ /*** ROUTINGS ***/
@@ -73,6 +74,37 @@ app.get('/aboutUs', (req, res) => {
return res.status(status.Ok); return res.status(status.Ok);
}); });
app.get('/forgotPassword', (req, res) => {
const error = req.session.error;
const reset = req.session.reset;
delete req.session.reset;
delete req.session.error;
res.render('forgotPass', { error: error, reset: reset });
return res.status(status.Ok);
});
// Reset with token given to user via email
app.get('/reset/:token', async (req, res) => {
const token = req.params.token;
const user = await users.findOne({
resetToken: token,
resetTokenExpires: { $gt: Date.now() },
});
if (!user) {
req.session.error = 'reset link not valid or has expired';
return res.redirect('/forgotPassword');
}
const error = req.session.error;
delete req.session.error;
res.render('resetPass', {
token: token,
errMessage: error,
});
});
// 404 handler - keep the actual notFound route please // 404 handler - keep the actual notFound route please
// REALLY DONT DELETE THIS // REALLY DONT DELETE THIS
app.get('/notFound', (req, res) => { app.get('/notFound', (req, res) => {
@@ -86,6 +118,8 @@ initDatabase().then(() => {
// Import authentication handler // Import authentication handler
app.use(require("./src/auth/authentication")(users)); app.use(require("./src/auth/authentication")(users));
app.use(require('./src/auth/forgotPass')(users));
// Import middleware & apply to user routes // Import middleware & apply to user routes
const middleware = require("./src/auth/middleware")(users); const middleware = require("./src/auth/middleware")(users);
+523 -1
View File
@@ -9,19 +9,38 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@google/genai": "^0.14.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"connect-mongo": "^5.1.0", "connect-mongo": "^5.1.0",
"crypto": "^1.0.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"joi": "^17.13.3", "joi": "^17.13.3",
"mongodb": "^6.16.0" "mongodb": "^6.16.0",
"nodemailer": "^7.0.3",
"sweetalert2": "^11.21.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "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": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -87,6 +106,15 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause" "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": { "node_modules/@types/webidl-conversions": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@@ -102,6 +130,15 @@
"@types/webidl-conversions": "*" "@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": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -209,12 +246,46 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "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": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "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": { "node_modules/bcrypt": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@@ -229,6 +300,14 @@
"node": ">= 10.0.0" "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": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -300,6 +379,19 @@
"node": ">=16.20.1" "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -415,6 +507,17 @@
"color-support": "bin.js" "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -483,6 +586,12 @@
"node": ">=6.6.0" "node": ">=6.6.0"
} }
}, },
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -500,6 +609,14 @@
} }
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -550,6 +667,14 @@
"node": ">= 0.4" "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": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -586,6 +711,14 @@
"node": ">= 0.8" "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": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -616,6 +749,20 @@
"node": ">= 0.4" "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": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -713,6 +860,38 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "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": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -773,6 +952,58 @@
"node": ">= 0.8" "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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -866,6 +1097,71 @@
"node": ">=10" "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": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -903,6 +1199,20 @@
"node": ">= 0.4" "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": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -937,6 +1247,43 @@
"node": ">= 6" "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": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -949,6 +1296,18 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -970,6 +1329,20 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -1123,6 +1496,17 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT" "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": { "node_modules/jake": {
"version": "10.9.2", "version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
@@ -1154,6 +1538,33 @@
"@sideway/pinpoint": "^2.0.0" "@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": { "node_modules/kruptein": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.7.tgz", "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.7.tgz",
@@ -1430,6 +1841,15 @@
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
}, },
"node_modules/nodemailer": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
@@ -1598,6 +2018,11 @@
"node": ">=16" "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": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1624,6 +2049,11 @@
"node": ">= 0.10" "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": { "node_modules/pstree.remy": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -1631,6 +2061,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -1990,6 +2429,15 @@
"node": ">=8" "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": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
@@ -2007,6 +2455,17 @@
"node": ">=10" "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": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2084,6 +2543,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -2099,6 +2564,18 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -2145,11 +2622,56 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "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": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC" "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"
}
} }
} }
} }
+5 -1
View File
@@ -18,14 +18,18 @@
}, },
"homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme", "homepage": "https://github.com/JoaquinPar/2800-202510-BBY14#readme",
"dependencies": { "dependencies": {
"@google/genai": "^0.14.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"connect-mongo": "^5.1.0", "connect-mongo": "^5.1.0",
"crypto": "^1.0.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"joi": "^17.13.3", "joi": "^17.13.3",
"mongodb": "^6.16.0" "mongodb": "^6.16.0",
"nodemailer": "^7.0.3",
"sweetalert2": "^11.21.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
+46 -13
View File
@@ -12,15 +12,23 @@ module.exports = (users) => {
const router = require("express").Router(); const router = require("express").Router();
router.get("/logout", (req, res) => { router.get("/logout", (req, res) => {
req.session.destroy(); req.session.destroy((err) => {
return res.redirect('/login'); 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) => { router.post("/login", async (req, res) => {
const credentialSchema = joi.object({ const credentialSchema = joi.object({
email: joi.string().email().required(), email: joi.string().email().required(),
password: joi.string().max(20).required(), password: joi.string().alphanum().max(20).required(),
}); });
const valid = credentialSchema.validate(req.body); const valid = credentialSchema.validate(req.body);
@@ -46,19 +54,28 @@ module.exports = (users) => {
req.session.authenticated = true; req.session.authenticated = true;
req.session.userId = user._id; req.session.userId = user._id;
req.session.email = req.body.email;
req.session.errMessage = ""; req.session.errMessage = "";
res.redirect("/home");
return res.status(status.Ok); req.session.save((err) => {
if (err) {
console.error("Failed to save session: ", err);
req.session.errMessage = "Failed to save session. Please try again.";
res.status(status.InternalServerError);
return res.redirect("/login");
}
res.status(status.Ok);
return res.redirect("/home");
});
}); });
}); });
router.post("/signup", async (req, res) => { router.post("/signup", async (req, res) => {
const userSchema = joi.object({ const userSchema = joi.object({
email: joi.string().email().required(), email: joi.string().email().required(),
name: joi.string().alphanum().max(20).required(), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20).required(),
password: joi.string().max(20).min(8).required(), password: joi.string().alphanum().max(20).min(8).required(),
repassword: joi.string().max(20).min(8).required(), repassword: joi.string().alphanum().max(20).min(8).required(),
}); });
const valid = userSchema.validate(req.body); const valid = userSchema.validate(req.body);
@@ -69,6 +86,13 @@ module.exports = (users) => {
return res.redirect("/signup"); return res.redirect("/signup");
} }
let exists = await users.findOne({ email: req.body.email }).then((exists) => exists);
if (exists) {
req.session.errMessage = "Email already in use";
res.status(status.BadRequest);
return res.redirect("/signup");
}
if (req.body.password != req.body.repassword) { if (req.body.password != req.body.repassword) {
req.session.errMessage = "Passwords must match"; req.session.errMessage = "Passwords must match";
res.status(status.BadRequest); res.status(status.BadRequest);
@@ -84,17 +108,26 @@ module.exports = (users) => {
financialData: false, financialData: false,
}).then((results, err) => { }).then((results, err) => {
if (err) { if (err) {
console.error(err); console.error("Error creating user on signup: ", err);
res.session.errMessage = "Internal server error"; res.session.errMessage = "Internal server error";
return res.status(status.InternalServerError).redirect("/signup"); return res.status(status.InternalServerError).redirect("/signup");
} }
req.session.authenticated = true; req.session.authenticated = true;
req.session.email = req.body.email;
req.session.userId = results.insertedId; req.session.userId = results.insertedId;
req.session.errMessage = ""; 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");
});
}); });
}); });
+119
View File
@@ -0,0 +1,119 @@
const express = require('express');
const crypto = require('crypto');
const joi = require('joi');
const nodeMail = require('nodemailer');
const bcrypt = require('bcrypt');
require('dotenv').config();
const PORT = process.env.PORT;
const transporter = nodeMail.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
}
});
module.exports = (users) => {
const router = express.Router();
router.post('/auth/resetPass', async (req, res) => {
const resetSchema = joi.object({
email: joi.string().email().required(),
});
req.session.error = '';
req.session.reset = '';
const valid = resetSchema.validate(req.body);
if (valid.error) {
req.session.error = 'invalid email';
return res.redirect('/forgotPassword')
}
const { email } = req.body;
const user = await users.findOne({ email });
if (!user) {
req.session.error = 'No user found'
return res.redirect('/forgotPassword');
}
const token = crypto.randomBytes(32).toString('hex');
console.log(`The reset token is ${token}`)
const expiration = Date.now() + 360000;
await users.updateOne({ email }, {
$set: { resetToken: token, resetTokenExpires: expiration }
});
const resetUrl = `http://localhost:${PORT}/reset/${token}`;
const mailSend = {
from: process.env.EMAIL_USER,
to: email,
subject: 'Password reset',
text: `reset your password here ${resetUrl} this link will expire within 1 hour`,
};
try {
await transporter.sendMail(mailSend);
req.session.reset = 'Reset link sent Check your email';
res.redirect('/forgotPassword');
} catch (err) {
console.log('there was an error', err);
res.status(500).send('email failed to send try again');
}
});
router.post('/resetLink', async (req, res) => {
const { token, password, confirmPassword, } = req.body;
const passwordSchema = joi.object({
password: joi.string().max(20).required(),
confirmPassword: joi.string().max(20).required(),
});
const valid = passwordSchema.validate({ password, confirmPassword });
if (valid.error) {
console.log("houston we have a problem"); // nice
req.session.error = 'Invalid input';
res.status(status.BadRequest);
return res.redirect(`/reset/${token}`);
}
if (!token || !password || !confirmPassword) {
req.session.error = 'field may be missing';
return res.redirect(`/reset/${token}`);
}
if (password !== confirmPassword) {
req.session.error = 'passwords do not match';
return res.redirect(`/reset/${token}`);
}
const user = await users.findOne({
resetToken: token,
resetTokenExpires: { $gt: Date.now() },
});
if (!user) {
req.session.error = 'Reset link is invalid.';
return res.redirect(`/reset`);
}
const hashPassword = await bcrypt.hash(password, 12);
await users.updateOne(
{
email: user.email
},
{
$set: {
password: hashPassword,
resetToken: '',
resetTokenExpires: 0,
},
}
);
req.session.success = 'Password has been reset';
res.redirect('/login');
});
return router;
}
+38 -9
View File
@@ -1,7 +1,8 @@
const status = require("../util/statuses"); const status = require("../util/statuses");
const session = require("express-session"); const session = require("express-session");
const ObjectId = require("mongodb").ObjectId;
// Get all user routes names // Get all names of user routes
let userRouter = require("../router/user")((req, res, next) => next(), null, null, null); let userRouter = require("../router/user")((req, res, next) => next(), null, null, null);
userRouter.stack.shift(); userRouter.stack.shift();
const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1]); const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1]);
@@ -12,31 +13,59 @@ const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1
*/ */
const createMiddleware = (users) => { const createMiddleware = (users) => {
return async (req, res, next) => { return async (req, res, next) => {
// Redirect not found pages to 404 page // Check if incoming request route exists in user routes
if (!userRoutes.includes(req.url.split("/")[1])) { // redirect to 404 if not
if (!userRoutes.includes(req.url.split("/")[1].split("?")[0])) {
return res.status(status.NotFound).redirect("/notFound"); 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"; req.session.errMessage = "Please login to view that resource";
res.redirect("/login"); res.redirect("/login");
return res.status(status.Unauthorized); return res.status(status.Unauthorized);
} }
if (!req.session.user) { 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) { if (!user) {
req.session.errMessage = "User not found"; return req.session.destroy((err) => {
res.redirect("/login"); if (err) {
return res.status(status.Unauthorized); 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; 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 = "An error occured, please login again.";
if (err) {
console.error("Failed to destroy session: ", err);
}
res.status(status.InternalServerError);
res.redirect("/login");
});
}
next();
});
} }
next(); next();
}; };
} };
module.exports = createMiddleware; module.exports = createMiddleware;
+54 -5
View File
@@ -8,6 +8,12 @@ const assetForms = [
* resetAll asset creation forms * resetAll asset creation forms
*/ */
function resetAll() { function resetAll() {
document.getElementById("dropdown-icon-button").innerHTML = `
<img src="/static/svgs/icons/Other.svg" class="h-4 w-4 me-2" alt="Other"> Other
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
`;
document.getElementById("create-other-asset-form").reset(); document.getElementById("create-other-asset-form").reset();
document.getElementById("create-saving-asset-form").reset(); document.getElementById("create-saving-asset-form").reset();
document.getElementById("create-stock-asset-form").reset(); document.getElementById("create-stock-asset-form").reset();
@@ -40,6 +46,7 @@ function resetRadio() {
const assetKeys = { const assetKeys = {
other: [ other: [
"name", "name",
"dropdown-icon-button",
"value", "value",
"description", "description",
"purchaseDate", "purchaseDate",
@@ -59,8 +66,17 @@ const assetKeys = {
/** /**
* lockAsset prevents edits to asset view modal * lockAsset prevents edits to asset view modal
* @param {string} assetId * @param {string} assetId
* @param {string} icon to defualt to
*/ */
function lockAsset(assetId) { function lockAsset(assetId, icon) {
let dropdown = document.getElementById(`dropdown-icon-button-${assetId}`);
if (dropdown) dropdown.innerHTML = `
<img src="/static/svgs/icons/${icon}.svg" class="h-4 w-4 me-2" alt="${icon}"> ${icon}
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
`;
document.getElementById(`${assetId}-form`).reset(); document.getElementById(`${assetId}-form`).reset();
const type = document.getElementById(`type-${assetId}`).value; const type = document.getElementById(`type-${assetId}`).value;
@@ -74,14 +90,15 @@ function lockAsset(assetId) {
document.getElementById(`save-${assetId}`).classList.add("cursor-not-allowed"); document.getElementById(`save-${assetId}`).classList.add("cursor-not-allowed");
document.getElementById(`edit-${assetId}`).innerHTML = "Edit"; document.getElementById(`edit-${assetId}`).innerHTML = "Edit";
document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId) }; document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId, icon) };
} }
/** /**
* unlockAsset allows edits to asset view modal * unlockAsset allows edits to asset view modal
* @param {string} assetId * @param {string} assetId
* @param {string} icon to defualt to
*/ */
function unlockAsset(assetId) { function unlockAsset(assetId, icon) {
const type = document.getElementById(`type-${assetId}`).value; const type = document.getElementById(`type-${assetId}`).value;
assetKeys[type].forEach((key) => { assetKeys[type].forEach((key) => {
@@ -92,8 +109,40 @@ function unlockAsset(assetId) {
document.getElementById(`save-${assetId}`).classList.remove("cursor-not-allowed"); document.getElementById(`save-${assetId}`).classList.remove("cursor-not-allowed");
document.getElementById(`save-${assetId}`).classList.add("cursor-pointer"); document.getElementById(`save-${assetId}`).classList.add("cursor-pointer");
document.getElementById(`edit-${assetId}`).innerHTML = "Cancel changes"; document.getElementById(`edit-${assetId}`).innerHTML = "Cancel";
document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId) }; document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId, icon) };
}
/**
* autoOpenCreate checks if popup param
* in url to auto open create popup
*/
function autoOpenCreate() {
const query = window.location.search;
const params = new URLSearchParams(query);
if (params.has("popup")) {
document.getElementById('create-asset-modal').showModal();
}
}
/**
* selectIcon updated selected icon while creating
* or modifying assets.
* @param {button element} selectedIcon
* @param {string} assetId
*/
function selectIcon(selectedIcon, assetId="") {
document.getElementById(`dropdown-icon-button${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value;
document.getElementById(`icon${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value
document.getElementById(`dropdown-icon-button${assetId != "" ? "-" : ""}${assetId}`).innerHTML = selectedIcon.innerHTML +
`
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
`;
} }
resetRadio(); resetRadio();
autoOpenCreate();
+40 -27
View File
@@ -5,40 +5,47 @@ function getLocation() {
} }
function update(data) { function update(data) {
document.getElementById("loading").style = "display: none"; if (data.data.message != "error") {
document.getElementById("currencyExchange").style = "display: flex"; 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").src = `/static/svgs/flags/${data.data.country}.svg`
document.getElementById("yourFlag").alt = data.data.country; document.getElementById("yourFlag").alt = data.data.country;
document.getElementById("flagTag").innerHTML = `(${data.data.country})`; document.getElementById("flagTag").innerHTML = `(${data.data.country})`;
document.getElementById("exchangeFlag").src = `/static/svgs/flags/USD.svg`; document.getElementById("exchangeFlag").src = `/static/svgs/flags/USD.svg`;
document.getElementById("exchangeFlag").alt = "USD"; document.getElementById("exchangeFlag").alt = "USD";
document.getElementById("exFlagTag").innerHTML = "(USD)"; document.getElementById("exFlagTag").innerHTML = "(USD)";
document.getElementById("dropdown-country-button").value = data.data.toCurrencyRates["USD"]; document.getElementById("dropdown-country-button").value = data.data.toCurrencyRates["USD"];
updateExchange(document.getElementById("dropdown-country-button").value); updateExchange(document.getElementById("dropdown-country-button").value);
const dropdown = document.getElementById("dropdown"); const dropdown = document.getElementById("dropdown");
let countries = Object.keys(data.data.toCurrencyRates); let countries = Object.keys(data.data.toCurrencyRates);
countries.forEach(item => { countries.forEach(item => {
if (dropdown.querySelector(`button[value="${data.data.toCurrencyRates[item]}"]`)) { if (dropdown.querySelector(`button[value="${data.data.toCurrencyRates[item]}"]`)) {
return; return;
} }
const listItem = document.createElement("li"); const listItem = document.createElement("li");
listItem.innerHTML = `<button onclick="switchButton(this)" type="button" value="${data.data.toCurrencyRates[item]}" class="country Button inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem"> listItem.innerHTML = `<button onclick="switchButton(this)" type="button" value="${data.data.toCurrencyRates[item]}" class="country Button inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem">
<span class="inline-flex items-center"> <span class="inline-flex items-center">
<img src="/static/svgs/flags/${item}.svg" class="h-4 w-4 me-2" alt="${item}"> (${item}) <img src="/static/svgs/flags/${item}.svg" class="h-4 w-4 me-2" alt="${item}"> (${item})
</span> </span>
</button>`; </button>`;
dropdown.appendChild(listItem); dropdown.appendChild(listItem);
}); });
} else {
// console.log(data); 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) { function switchButton(clickedButton) {
@@ -67,5 +74,11 @@ async function getLatestExchange(position) {
} }
function error(err) { function error(err) {
console.error("Geolocation error: ", err); const data = {
data: {
message: "error"
}
}
update(data);
} }
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M312 32c-13.3 0-24 10.7-24 24s10.7 24 24 24l25.7 0 34.6 64-149.4 0-27.4-38C191 99.7 183.7 96 176 96l-56 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l43.7 0 22.1 30.7-26.6 53.1c-10-2.5-20.5-3.8-31.2-3.8C57.3 224 0 281.3 0 352s57.3 128 128 128c65.3 0 119.1-48.9 127-112l49 0c8.5 0 16.3-4.5 20.7-11.8l84.8-143.5 21.7 40.1C402.4 276.3 384 312 384 352c0 70.7 57.3 128 128 128s128-57.3 128-128s-57.3-128-128-128c-13.5 0-26.5 2.1-38.7 6L375.4 48.8C369.8 38.4 359 32 347.2 32L312 32zM458.6 303.7l32.3 59.7c6.3 11.7 20.9 16 32.5 9.7s16-20.9 9.7-32.5l-32.3-59.7c3.6-.6 7.4-.9 11.2-.9c39.8 0 72 32.2 72 72s-32.2 72-72 72s-72-32.2-72-72c0-18.6 7-35.5 18.6-48.3zM133.2 368l65 0c-7.3 32.1-36 56-70.2 56c-39.8 0-72-32.2-72-72s32.2-72 72-72c1.7 0 3.4 .1 5.1 .2l-24.2 48.5c-9 18.1 4.1 39.4 24.3 39.4zm33.7-48l50.7-101.3 72.9 101.2-.1 .1-123.5 0zm90.6-128l108.5 0L317 274.8 257.4 192z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/></svg>

After

Width:  |  Height:  |  Size: 626 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M149.1 64.8L138.7 96 64 96C28.7 96 0 124.7 0 160L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-256c0-35.3-28.7-64-64-64l-74.7 0L362.9 64.8C356.4 45.2 338.1 32 317.4 32L194.6 32c-20.7 0-39 13.2-45.5 32.8zM256 192a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>

After

Width:  |  Height:  |  Size: 489 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M135.2 117.4L109.1 192l293.8 0-26.1-74.6C372.3 104.6 360.2 96 346.6 96L165.4 96c-13.6 0-25.7 8.6-30.2 21.4zM39.6 196.8L74.8 96.3C88.3 57.8 124.6 32 165.4 32l181.2 0c40.8 0 77.1 25.8 90.6 64.3l35.2 100.5c23.2 9.6 39.6 32.5 39.6 59.2l0 144 0 48c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-48L96 400l0 48c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-48L0 256c0-26.7 16.4-49.6 39.6-59.2zM128 288a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm288 32a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg>

After

Width:  |  Height:  |  Size: 713 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M512 80c0 18-14.3 34.6-38.4 48c-29.1 16.1-72.5 27.5-122.3 30.9c-3.7-1.8-7.4-3.5-11.3-5C300.6 137.4 248.2 128 192 128c-8.3 0-16.4 .2-24.5 .6l-1.1-.6C142.3 114.6 128 98 128 80c0-44.2 86-80 192-80S512 35.8 512 80zM160.7 161.1c10.2-.7 20.7-1.1 31.3-1.1c62.2 0 117.4 12.3 152.5 31.4C369.3 204.9 384 221.7 384 240c0 4-.7 7.9-2.1 11.7c-4.6 13.2-17 25.3-35 35.5c0 0 0 0 0 0c-.1 .1-.3 .1-.4 .2c0 0 0 0 0 0s0 0 0 0c-.3 .2-.6 .3-.9 .5c-35 19.4-90.8 32-153.6 32c-59.6 0-112.9-11.3-148.2-29.1c-1.9-.9-3.7-1.9-5.5-2.9C14.3 274.6 0 258 0 240c0-34.8 53.4-64.5 128-75.4c10.5-1.5 21.4-2.7 32.7-3.5zM416 240c0-21.9-10.6-39.9-24.1-53.4c28.3-4.4 54.2-11.4 76.2-20.5c16.3-6.8 31.5-15.2 43.9-25.5l0 35.4c0 19.3-16.5 37.1-43.8 50.9c-14.6 7.4-32.4 13.7-52.4 18.5c.1-1.8 .2-3.5 .2-5.3zm-32 96c0 18-14.3 34.6-38.4 48c-1.8 1-3.6 1.9-5.5 2.9C304.9 404.7 251.6 416 192 416c-62.8 0-118.6-12.6-153.6-32C14.3 370.6 0 354 0 336l0-35.4c12.5 10.3 27.6 18.7 43.9 25.5C83.4 342.6 135.8 352 192 352s108.6-9.4 148.1-25.9c7.8-3.2 15.3-6.9 22.4-10.9c6.1-3.4 11.8-7.2 17.2-11.2c1.5-1.1 2.9-2.3 4.3-3.4l0 3.4 0 5.7 0 26.3zm32 0l0-32 0-25.9c19-4.2 36.5-9.5 52.1-16c16.3-6.8 31.5-15.2 43.9-25.5l0 35.4c0 10.5-5 21-14.9 30.9c-16.3 16.3-45 29.7-81.3 38.4c.1-1.7 .2-3.5 .2-5.3zM192 448c56.2 0 108.6-9.4 148.1-25.9c16.3-6.8 31.5-15.2 43.9-25.5l0 35.4c0 44.2-86 80-192 80S0 476.2 0 432l0-35.4c12.5 10.3 27.6 18.7 43.9 25.5C83.4 438.6 135.8 448 192 448z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M192 64C86 64 0 150 0 256S86 448 192 448l256 0c106 0 192-86 192-192s-86-192-192-192L192 64zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24l0 32 32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0 0 32c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-32-32 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l32 0 0-32z"/></svg>

After

Width:  |  Height:  |  Size: 602 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 352c0 35.3 28.7 64 64 64l176 0-10.7 32L160 448c-17.7 0-32 14.3-32 32s14.3 32 32 32l256 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-69.3 0L336 416l176 0c35.3 0 64-28.7 64-64l0-288c0-35.3-28.7-64-64-64L64 0zM512 64l0 224L64 288 64 64l448 0z"/></svg>

After

Width:  |  Height:  |  Size: 491 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M176 24c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40c-35.3 0-64 28.7-64 64l-40 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l40 0 0 56-40 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l40 0 0 56-40 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l40 0c0 35.3 28.7 64 64 64l0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40 56 0 0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40 56 0 0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40c35.3 0 64-28.7 64-64l40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0 0-56 40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0 0-56 40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0c0-35.3-28.7-64-64-64l0-40c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40-56 0 0-40c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40-56 0 0-40zM160 128l192 0c17.7 0 32 14.3 32 32l0 192c0 17.7-14.3 32-32 32l-192 0c-17.7 0-32-14.3-32-32l0-192c0-17.7 14.3-32 32-32zm192 32l-192 0 0 192 192 0 0-192z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M349.4 44.6c5.9-13.7 1.5-29.7-10.6-38.5s-28.6-8-39.9 1.8l-256 224c-10 8.8-13.6 22.9-8.9 35.3S50.7 288 64 288l111.5 0L98.6 467.4c-5.9 13.7-1.5 29.7 10.6 38.5s28.6 8 39.9-1.8l256-224c10-8.8 13.6-22.9 8.9-35.3s-16.6-20.7-30-20.7l-111.5 0L349.4 44.6z"/></svg>

After

Width:  |  Height:  |  Size: 477 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M575.8 255.5c0 18-15 32.1-32 32.1l-32 0 .7 160.2c0 2.7-.2 5.4-.5 8.1l0 16.2c0 22.1-17.9 40-40 40l-16 0c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1L416 512l-24 0c-22.1 0-40-17.9-40-40l0-24 0-64c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32 14.3-32 32l0 64 0 24c0 22.1-17.9 40-40 40l-24 0-31.9 0c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2l-16 0c-22.1 0-40-17.9-40-40l0-112c0-.9 0-1.9 .1-2.8l0-69.7-32 0c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"/></svg>

After

Width:  |  Height:  |  Size: 715 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 32C92.7 32 64 60.7 64 96l0 256 64 0 0-256 384 0 0 256 64 0 0-256c0-35.3-28.7-64-64-64L128 32zM19.2 384C8.6 384 0 392.6 0 403.2C0 445.6 34.4 480 76.8 480l486.4 0c42.4 0 76.8-34.4 76.8-76.8c0-10.6-8.6-19.2-19.2-19.2L19.2 384z"/></svg>

After

Width:  |  Height:  |  Size: 459 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 112.5L0 422.3c0 18 10.1 35 27 41.3c87 32.5 174 10.3 261-11.9c79.8-20.3 159.6-40.7 239.3-18.9c23 6.3 48.7-9.5 48.7-33.4l0-309.9c0-18-10.1-35-27-41.3C462 15.9 375 38.1 288 60.3C208.2 80.6 128.4 100.9 48.7 79.1C25.6 72.8 0 88.6 0 112.5zM128 416l-64 0 0-64c35.3 0 64 28.7 64 64zM64 224l0-64 64 0c0 35.3-28.7 64-64 64zM448 352c0-35.3 28.7-64 64-64l0 64-64 0zm64-192c-35.3 0-64-28.7-64-64l64 0 0 64zM384 256c0 61.9-43 112-96 112s-96-50.1-96-112s43-112 96-112s96 50.1 96 112zM252 208c0 9.7 6.9 17.7 16 19.6l0 48.4-4 0c-11 0-20 9-20 20s9 20 20 20l24 0 24 0c11 0 20-9 20-20s-9-20-20-20l-4 0 0-68c0-11-9-20-20-20l-16 0c-11 0-20 9-20 20z"/></svg>

After

Width:  |  Height:  |  Size: 860 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M280 32c-13.3 0-24 10.7-24 24s10.7 24 24 24l57.7 0 16.4 30.3L256 192l-45.3-45.3c-12-12-28.3-18.7-45.3-18.7L64 128c-17.7 0-32 14.3-32 32l0 32 96 0c88.4 0 160 71.6 160 160c0 11-1.1 21.7-3.2 32l70.4 0c-2.1-10.3-3.2-21-3.2-32c0-52.2 25-98.6 63.7-127.8l15.4 28.6C402.4 276.3 384 312 384 352c0 70.7 57.3 128 128 128s128-57.3 128-128s-57.3-128-128-128c-13.5 0-26.5 2.1-38.7 6L418.2 128l61.8 0c17.7 0 32-14.3 32-32l0-32c0-17.7-14.3-32-32-32l-20.4 0c-7.5 0-14.7 2.6-20.5 7.4L391.7 78.9l-14-26c-7-12.9-20.5-21-35.2-21L280 32zM462.7 311.2l28.2 52.2c6.3 11.7 20.9 16 32.5 9.7s16-20.9 9.7-32.5l-28.2-52.2c2.3-.3 4.7-.4 7.1-.4c35.3 0 64 28.7 64 64s-28.7 64-64 64s-64-28.7-64-64c0-15.5 5.5-29.7 14.7-40.8zM187.3 376c-9.5 23.5-32.5 40-59.3 40c-35.3 0-64-28.7-64-64s28.7-64 64-64c26.9 0 49.9 16.5 59.3 40l66.4 0C242.5 268.8 190.5 224 128 224C57.3 224 0 281.3 0 352s57.3 128 128 128c62.5 0 114.5-44.8 125.8-104l-66.4 0zM128 384a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M184 48l144 0c4.4 0 8 3.6 8 8l0 40L176 96l0-40c0-4.4 3.6-8 8-8zm-56 8l0 40L64 96C28.7 96 0 124.7 0 160l0 96 192 0 128 0 192 0 0-96c0-35.3-28.7-64-64-64l-64 0 0-40c0-30.9-25.1-56-56-56L184 0c-30.9 0-56 25.1-56 56zM512 288l-192 0 0 32c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32-14.3-32-32l0-32L0 288 0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-128z"/></svg>

After

Width:  |  Height:  |  Size: 584 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M16 64C16 28.7 44.7 0 80 0L304 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L80 512c-35.3 0-64-28.7-64-64L16 64zM224 448a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zM304 64L80 64l0 320 224 0 0-320z"/></svg>

After

Width:  |  Height:  |  Size: 423 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M470.7 9.4c3 3.1 5.3 6.6 6.9 10.3s2.4 7.8 2.4 12.2c0 0 0 .1 0 .1c0 0 0 0 0 0l0 96c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-18.7L310.6 214.6c-11.8 11.8-30.8 12.6-43.5 1.7L176 138.1 84.8 216.3c-13.4 11.5-33.6 9.9-45.1-3.5s-9.9-33.6 3.5-45.1l112-96c12-10.3 29.7-10.3 41.7 0l89.5 76.7L370.7 64 352 64c-17.7 0-32-14.3-32-32s14.3-32 32-32l96 0s0 0 0 0c8.8 0 16.8 3.6 22.6 9.3l.1 .1zM0 304c0-26.5 21.5-48 48-48l416 0c26.5 0 48 21.5 48 48l0 160c0 26.5-21.5 48-48 48L48 512c-26.5 0-48-21.5-48-48L0 304zM48 416l0 48 48 0c0-26.5-21.5-48-48-48zM96 304l-48 0 0 48c26.5 0 48-21.5 48-48zM464 416c-26.5 0-48 21.5-48 48l48 0 0-48zM416 304c0 26.5 21.5 48 48 48l0-48-48 0zm-96 80a64 64 0 1 0 -128 0 64 64 0 1 0 128 0z"/></svg>

After

Width:  |  Height:  |  Size: 926 B

+72 -52
View File
@@ -1,10 +1,13 @@
const getRates = require("../util/exchangeRate"); const getRates = require("../util/exchangeRate");
const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations"); const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations");
const suggestions = require("../util/suggestions");
const status = require("../util/statuses"); const status = require("../util/statuses");
const ObjectId = require('mongodb').ObjectId; const ObjectId = require('mongodb').ObjectId;
const session = require("express-session"); const session = require("express-session");
const bcrypt = require("bcrypt"); const bcrypt = require("bcrypt");
const path = require("path");
const joi = require("joi"); const joi = require("joi");
const fs = require("fs");
const salt = 12; const salt = 12;
/** /**
@@ -20,6 +23,7 @@ const getAssetSchema = (type) => {
case "other": case "other":
assetSchema = joi.object({ assetSchema = joi.object({
type: joi.string().valid("other", "stock", "saving").required(), type: joi.string().valid("other", "stock", "saving").required(),
icon: joi.string().alphanum().required(),
name: joi.string().alphanum().min(3).max(30).required(), name: joi.string().alphanum().min(3).max(30).required(),
value: joi.number().min(0).required(), value: joi.number().min(0).required(),
purchaseDate: joi.date().required(), purchaseDate: joi.date().required(),
@@ -61,6 +65,10 @@ const getAssetSchema = (type) => {
module.exports = (middleware, users, plans, assets) => { module.exports = (middleware, users, plans, assets) => {
const router = require("express").Router(); const router = require("express").Router();
// Create list of icon filenames
let icons = fs.readdirSync(path.join(__dirname, "../public/svgs/icons"));
icons = icons.map((icon) => icon.split(".")[0]);
router.use(middleware); router.use(middleware);
router.get('/home', async (req, res) => { router.get('/home', async (req, res) => {
@@ -78,33 +86,32 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.get('/assets', async (req, res) => { 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', { res.render('assets', {
user: req.session.user, user: req.session.user,
errMessage: req.session.errMessage, errMessage: req.session.errMessage,
assets: userAssets, assets: userAssets,
geoData: req.session.geoData }); geoData: req.session.geoData,
icons: icons,
});
return res.status(status.Ok); return res.status(status.Ok);
}); });
router.get('/plans', async (req, res) => { router.get('/plans', async (req, res) => {
try { try {
// console.log(new ObjectId(req.session.user._id)); const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray();
const userPlansFromDB = await plans.find({userId: new ObjectId(req.session.user._id) }).toArray();
// console.log(userPlansFromDB);
// Use a for...of loop for proper async/await behavior in series for updates
for (const plan of userPlansFromDB) { for (const plan of userPlansFromDB) {
const percentage = await calculatePlanProgress(plan, assets, req.session.user._id); 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(); const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray();
res.render('plans', { res.render('plans', {
user: req.session.user, user: req.session.user,
plans: updatedUserPlans, // Send the most up-to-date plans plans: updatedUserPlans,
geoData: req.session.geoData geoData: req.session.geoData
}); });
} catch (err) { } catch (err) {
@@ -118,35 +125,27 @@ module.exports = (middleware, users, plans, assets) => {
try { try {
const planId = req.params.id; 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)) { if (!ObjectId.isValid(planId)) {
req.session.errMessage = "Invalid plan ID format."; req.session.errMessage = "Invalid plan ID format.";
return res.status(status.BadRequest).redirect('/plans'); 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) { 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."; req.session.errMessage = "Plan not found or you do not have permission to view it.";
return res.status(status.NotFound).redirect('/plans'); return res.status(status.NotFound).redirect('/plans');
} }
// console.log("Found plan:", plan);
// 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', { res.render('planDetail', {
user: req.session.user, user: req.session.user,
plan: plan, // This plan object will have the progress from the database plan: plan,
geoData: req.session.geoData, geoData: req.session.geoData,
assets: userAssets, assets: userAssets,
suggestions: await suggestions.generateSuggestions(),
}); });
} catch (err) { } catch (err) {
@@ -157,7 +156,7 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.get('/newPlan', (req, res) => { router.get('/newPlan', (req, res) => {
if(!req.session.user.financialData){ if (!req.session.user.financialData || !req.session.user) {
req.session.errMessage = "Please complete your financial data before creating a plan."; req.session.errMessage = "Please complete your financial data before creating a plan.";
return res.status(status.Unauthorized).redirect('/questionnaire'); return res.status(status.Unauthorized).redirect('/questionnaire');
} }
@@ -189,7 +188,7 @@ module.exports = (middleware, users, plans, assets) => {
return; return;
} }
const newPlan = { const newPlan = {
userId: new ObjectId(req.session.user._id), userId: new ObjectId(req.session.userId),
name: value.name, name: value.name,
retirementAge: value.retirementAge, retirementAge: value.retirementAge,
retirementExpenses: value.retirementExpenses, retirementExpenses: value.retirementExpenses,
@@ -198,18 +197,24 @@ module.exports = (middleware, users, plans, assets) => {
progress: "0" progress: "0"
}; };
try{ 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 = ""; req.session.errMessage = "";
res.redirect('/plans'); res.redirect('/plans');
} }
catch(err){ catch (err) {
console.error("Error saving plan:", err); console.error("Error saving plan:", err);
req.session.errMessage = "An error occurred while saving your plan. Please try again."; req.session.errMessage = "An error occurred while saving your plan. Please try again.";
res.status(status.InternalServerError).redirect("/newPlan"); res.status(status.InternalServerError).redirect("/newPlan");
} }
}); });
router.post('/fact', async (req, res) => {
const factInput = req.body.fact;
const fact = await suggestions.generateFact(factInput);
return res.status(status.Ok).json({ fact });
});
router.get('/more', (req, res) => { router.get('/more', (req, res) => {
res.render('more', { res.render('more', {
user: req.session.user, user: req.session.user,
@@ -246,8 +251,6 @@ module.exports = (middleware, users, plans, assets) => {
}); });
router.post('/questionnaire', (req, res) => { router.post('/questionnaire', (req, res) => {
// console.log("Questionnaire POST body:", req.body);
const questionnaireSchema = joi.object({ const questionnaireSchema = joi.object({
dob: joi.date().required(), dob: joi.date().required(),
education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(), education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(),
@@ -269,7 +272,7 @@ module.exports = (middleware, users, plans, assets) => {
} }
users.updateOne( users.updateOne(
{ _id: new ObjectId(req.session.user._id) }, { _id: new ObjectId(req.session.userId) },
{ {
$set: { $set: {
financialData: true, financialData: true,
@@ -284,18 +287,24 @@ module.exports = (middleware, users, plans, assets) => {
} }
).then((result) => { ).then((result) => {
if (result.matchedCount === 0) { 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."; req.session.errMessage = "User session invalid. Please log in again.";
res.status(status.NotFound).redirect("/login"); res.status(status.NotFound).redirect("/login");
return; return;
} }
if (result.modifiedCount === 0 && result.matchedCount === 1) { 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.user.financialData = true;
req.session.errMessage = ""; req.session.errMessage = "";
res.status(status.Ok).redirect("/home");
req.session.save(err => {
if (err) {
res.status(status.InternalServerError).redirect("/plans");
}
res.status(status.Ok).redirect("/plans");
});
}).catch(err => { }).catch(err => {
console.error("Error updating questionnaire in database:", err); console.error("Error updating questionnaire in database:", err);
@@ -307,9 +316,9 @@ module.exports = (middleware, users, plans, assets) => {
router.post("/updateAccount", async (req, res) => { router.post("/updateAccount", async (req, res) => {
const accountSchema = joi.object({ const accountSchema = joi.object({
email: joi.string().email(), email: joi.string().email(),
name: joi.string().alphanum().max(20), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20),
password: joi.string().max(20).min(8), password: joi.string().alphanum().max(20).min(8),
repassword: joi.string().max(20).min(8), repassword: joi.string().alphanum().max(20).min(8),
}); });
const valid = accountSchema.validate(req.body); const valid = accountSchema.validate(req.body);
@@ -334,17 +343,17 @@ module.exports = (middleware, users, plans, assets) => {
} }
users.updateOne( users.updateOne(
{ email: req.session.email }, { _id: new ObjectId(req.session.userId) },
{ $set: update } { $set: update }
).then((result) => { ).then((result) => {
if (result.matchedCount === 0) { 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."; req.session.errMessage = "User session invalid. Please log in again.";
res.status(status.NotFound).redirect("/login"); res.status(status.NotFound).redirect("/login");
return; return;
} }
if (result.modifiedCount === 0 && result.matchedCount === 1) { 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 = ""; req.session.errMessage = "";
@@ -371,12 +380,12 @@ module.exports = (middleware, users, plans, assets) => {
if (valid.err) { if (valid.err) {
req.session.errMessage = "Invalid input", req.session.errMessage = "Invalid input",
res.status(status.BadRequest); res.status(status.BadRequest);
return res.redirect("/assets"); return res.redirect("/assets");
} }
let newAsset = { let newAsset = {
userId: new ObjectId(req.session.user._id), userId: new ObjectId(req.session.userId),
...req.body, ...req.body,
updatedAt: new Date(), updatedAt: new Date(),
}; };
@@ -388,6 +397,7 @@ module.exports = (middleware, users, plans, assets) => {
newAsset.name = `${newAsset.ticker} Stock`; newAsset.name = `${newAsset.ticker} Stock`;
} }
newAsset.value = parseFloat(newAsset.value); newAsset.value = parseFloat(newAsset.value);
newAsset.icon = type == "stock" ? "Stock" : type == "saving" ? "Coins" : newAsset.icon;
assets.insertOne(newAsset, (err, _) => { assets.insertOne(newAsset, (err, _) => {
if (err) { if (err) {
@@ -415,13 +425,13 @@ module.exports = (middleware, users, plans, assets) => {
if (valid.err) { if (valid.err) {
req.session.errMessage = "Invalid input", req.session.errMessage = "Invalid input",
res.status(status.BadRequest); res.status(status.BadRequest);
return res.redirect("/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", req.session.errMessage = "Cannot change asset owner",
res.status(status.BadRequest); res.status(status.BadRequest);
return res.redirect("/assets"); return res.redirect("/assets");
} }
@@ -433,7 +443,8 @@ module.exports = (middleware, users, plans, assets) => {
update.name = `${update.ticker} Stock`; update.name = `${update.ticker} Stock`;
} }
update.value = parseFloat(update.value); update.value = parseFloat(update.value);
delete update.id delete update.id;
delete update.userId;
assets.updateOne( assets.updateOne(
{ "_id": new ObjectId(req.body.id) }, { "_id": new ObjectId(req.body.id) },
@@ -488,20 +499,20 @@ module.exports = (middleware, users, plans, assets) => {
router.post("/deleteUser", (req, res) => { router.post("/deleteUser", (req, res) => {
// not as critical if results aren't as expected only if crashing // 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); console.error("Error deleting user assets: ", err);
req.session.errMessage = "An error occured while deleting your account. Please try again."; req.session.errMessage = "An error occured while deleting your account. Please try again.";
return res.status(status.InternalServerError).redirect("/profile"); 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); console.error("Error deleting user assets: ", err);
req.session.errMessage = "An error occured while deleting your account. Please try again."; req.session.errMessage = "An error occured while deleting your account. Please try again.";
return res.status(status.InternalServerError).redirect("/profile"); return res.status(status.InternalServerError).redirect("/profile");
}); });
users.deleteOne( users.deleteOne(
{ _id: new ObjectId(req.session.user._id) }, { _id: new ObjectId(req.session.userId) },
).then((result) => { ).then((result) => {
if (result.deletedCount === 0) { if (result.deletedCount === 0) {
console.error(`User not found: ${req.body.id}`); console.error(`User not found: ${req.body.id}`);
@@ -524,6 +535,7 @@ module.exports = (middleware, users, plans, assets) => {
return res.status(status.InternalServerError).redirect("/profile"); return res.status(status.InternalServerError).redirect("/profile");
}); });
}); });
router.get("/exRates/:lat/:lon", async (req, res) => { router.get("/exRates/:lat/:lon", async (req, res) => {
if (!req.session.geoData.country) { if (!req.session.geoData.country) {
const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${req.params.lat},${req.params.lon}&result_type=country&key=${process.env.GEOLOCATION_API}`); const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${req.params.lat},${req.params.lon}&result_type=country&key=${process.env.GEOLOCATION_API}`);
@@ -531,11 +543,19 @@ module.exports = (middleware, users, plans, assets) => {
country = data.results[0].formatted_address; country = data.results[0].formatted_address;
let results = await getRates(country); let results = await getRates(country);
req.session.geoData = {
country: results.abbreviation, if (!results.exRates) {
toCurrencyRates: results.exRates, req.session.geoData = {
geoData: req.session.geoData message: "error"
}
} else {
req.session.geoData = {
country: results.abbreviation,
toCurrencyRates: results.exRates,
geoData: req.session.geoData
}
} }
} }
return res.status(status.Ok).send({ data: req.session.geoData }); return res.status(status.Ok).send({ data: req.session.geoData });
+24
View File
@@ -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
};
+63 -14
View File
@@ -2,7 +2,6 @@
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main> <main>
<!-- Create Asset popup Modal --> <!-- Create Asset popup Modal -->
<dialog id="create-asset-modal" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md"> <dialog id="create-asset-modal" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
@@ -38,7 +37,31 @@
<!-- Create other asset form --> <!-- Create other asset form -->
<form method="POST" action="/createAsset" id="create-other-asset-form"> <form method="POST" action="/createAsset" id="create-other-asset-form">
<input style="display: none;" value="other" name="type" type="text"> <input style="display: none;" id="type" value="other" name="type" type="text">
<input style="display: none;" id="icon" value="Other" name="icon" type="text">
<label for="name" class="block text-sm font-medium text-gray-700 mt-2">Select an Icon</label>
<div class="flex items-center">
<button id="dropdown-icon-button" value="" data-dropdown-toggle="dropdown-icons" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" type="button">
<img src="/static/svgs/icons/Other.svg" class="h-4 w-4 me-2" alt="Other"> Other
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
</button>
<div id="dropdown-icons" class="overflow-y-auto h-40 z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-32 dark:bg-gray-700">
<ul id="dropdown" class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdown-icon-button">
<% icons.forEach(icon => { %>
<li>
<button onclick="selectIcon(this)" type="button" value="<%= icon %>" class="inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem">
<span class="inline-flex items-center">
<img src="/static/svgs/icons/<%= icon %>.svg" class="h-4 w-4 me-2" alt="<%= icon %>"> (<%= icon %>)
</span>
</button>
</li>
<% }); %>
</ul>
</div>
</div>
<label for="name" class="block text-sm font-medium text-gray-700 mt-2">Asset Name</label> <label for="name" class="block text-sm font-medium text-gray-700 mt-2">Asset Name</label>
<input required id="name" type="text" name="name" minlength="3" maxlength="30" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> <input required id="name" type="text" name="name" minlength="3" maxlength="30" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@@ -178,7 +201,9 @@
<button onclick="document.getElementById('<%= asset._id %>-modal').showModal()" style="width: 100%; height: 100%;" class="px-4 py-4 cursor-pointer"> <button onclick="document.getElementById('<%= asset._id %>-modal').showModal()" style="width: 100%; height: 100%;" class="px-4 py-4 cursor-pointer">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="mx-2">Icon</div> <div class="mx-2">
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-6 w-6 me-2" alt="<%= asset.icon %>">
</div>
<div class="mx-2"><h3><%= asset.name %></h3></div> <div class="mx-2"><h3><%= asset.name %></h3></div>
</div> </div>
<div> <div>
@@ -237,7 +262,7 @@
<form method="dialog" style="width: 100%;"> <form method="dialog" style="width: 100%;">
<button <button
style="width: 100%;" style="width: 100%;"
onclick="lockAsset('<%= asset._id %>')" onclick="lockAsset('<%= asset._id %>', '<%= asset.icon %>')"
type="submit" type="submit"
class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer" class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
> >
@@ -259,24 +284,52 @@
<br> <br>
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div> <% if (asset.type == "other") { %>
ICON <div class="flex items-center">
</div> <button id="dropdown-icon-button-<%= asset._id %>" value="" data-dropdown-toggle="dropdown-icons-<%= asset._id %>" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" type="button">
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-4 w-4 me-2" alt="<%= asset.icon %>"> <%= asset.icon %>
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
</button>
<div id="dropdown-icons-<%= asset._id %>" class="overflow-y-auto h-40 z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-32 dark:bg-gray-700">
<ul id="dropdown-<%= asset._id %>" class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdown-icon-button-<%= asset._id %>">
<% icons.forEach(icon => { %>
<li>
<button onclick="selectIcon(this, '<%= asset._id %>')" type="button" value="<%= icon %>" class="inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem">
<span class="inline-flex items-center">
<img src="/static/svgs/icons/<%= icon %>.svg" class="h-4 w-4 me-2" alt="<%= icon %>"> (<%= icon %>)
</span>
</button>
</li>
<% }); %>
</ul>
</div>
</div>
<% } else { %>
<button disabled class="inline-flex items-center py-2.5 px-10 text-sm font-medium text-center border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" type="button">
<img src="/static/svgs/icons/<%= asset.icon %>.svg" class="h-4 w-4" alt="<%= asset.icon %>">
</button>
<% } %>
<h2 class="text-xl font-medium tracking-tight leading-none mt-3"><%= String(asset.type).charAt(0).toUpperCase() + String(asset.type).slice(1); %> Asset</h2>
<div> <div>
<button <button
id="edit-<%= asset._id %>" id="edit-<%= asset._id %>"
onclick="unlockAsset('<%= asset._id %>')" onclick="unlockAsset('<%= asset._id %>')"
class="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer" class="py-3 px-8 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer"
> >
Edit Edit
</button> </button>
</div> </div>
</div> </div>
<br>
<form id="<%= asset._id %>-form" method="POST" action="/updateAsset"> <form id="<%= asset._id %>-form" method="POST" action="/updateAsset">
<input id="type-<%= asset._id %>" name="type" type="text" hidden value="<%= asset.type %>"> <input id="type-<%= asset._id %>" name="type" type="text" hidden value="<%= asset.type %>">
<input id="id-<%= asset._id %>" name="id" type="text" hidden value="<%= asset._id %>"> <input id="id-<%= asset._id %>" name="id" type="text" hidden value="<%= asset._id %>">
<input id="userId-<%= asset.userId %>" name="userId" type="text" hidden value="<%= asset.userId %>"> <input id="userId-<%= asset.userId %>" name="userId" type="text" hidden value="<%= asset.userId %>">
<input id="icon-<%= asset._id %>" name="icon" type="text" hidden value="<%= asset.icon %>">
<% if (asset.type != "stock") { %> <% if (asset.type != "stock") { %>
<label for="name" class="block text-sm font-medium text-gray-700 mt-6">Name</label> <label for="name" class="block text-sm font-medium text-gray-700 mt-6">Name</label>
@@ -329,15 +382,11 @@
<script src="/static/scripts/assetManager.js"></script> <script src="/static/scripts/assetManager.js"></script>
<script> <script>
// Ensure all assets popup modals are locked and reset on page load // Ensure all assets popup modals are locked and reset on page load
let assetIds = [];
<% assets.forEach((asset) => { %> <% assets.forEach((asset) => { %>
assetIds.push("<%= asset._id %>"); lockAsset("<%= asset._id %>", "<%= asset.icon %>");
<% }) %> <% }) %>
// EJS or JS may say this is an error above.
// I assure you its not, just leave it be.
assetIds.forEach((id) => lockAsset(id));
</script> </script>
<%- include("./partials/navBar") %> <%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+112 -100
View File
@@ -1,104 +1,116 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/header") %> <%- include("./partials/header") %>
<main> <main>
<!-- Buttons --> <!-- Buttons -->
<div class="flex justify-end gap-4 px-5 py-6"> <div class="flex justify-end gap-4 px-5 py-6">
<button class="cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800" type="button">Add Asset</button> <a href="/assets?popup"
<a href="/newPlan" class="cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800" type="button">Create new plan </a> class="text-center cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800"
</div> type="button">Add Asset</a>
<!--This is the cards for plans and other things--> <a href="/newPlan"
<div class="mt-12"> class="text-center cursor-pointer min-w-[150px] bg-green-600 px-4 py-2 text-white rounded hover:bg-green-800"
<div class="mb-12 grid gap-y-10 gap-x-6 md:grid-cols-2 mr-3 ml-3 xl:grid-cols-4"> type="button">Create new plan </a>
<!--box to put logo icon if we want on in a box-->
<div class=" relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<!-- <div -->
<!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-blue-600 to-blue-400 text-white shadow-blue-500/40 shadow-lg absolute mt-2 grid h-16 w-16 place-items-center"> -->
<!-- </div> -->
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div> </div>
<div class="border-t border-blue-gray-50 p-10"> <!--This is the cards for plans and other things-->
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> <div class="mt-12">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than lastweek <!--box to put logo icon if we want on in a box-->
</p> <!-- <div -->
</div> <!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-blue-600 to-blue-400 text-white shadow-blue-500/40 shadow-lg absolute mt-2 grid h-16 w-16 place-items-center"> -->
</div> <!-- </div> -->
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md"> <!--This is the cards for plans and other things-->
<div class="p-4 flex items-center justify-between"> <div class="mt-12">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600"> <div class="mb-12 grid gap-y-10 gap-x-6 md:grid-cols-2 mr-3 ml-3 xl:grid-cols-4">
Retirement goal</p> <!--box to put logo icon if we want on in a box-->
<h4 <div class=" relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> <!-- <div -->
$53k</h4> <!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-blue-600 to-blue-400 text-white shadow-blue-500/40 shadow-lg absolute mt-2 grid h-16 w-16 place-items-center"> -->
</div> <!-- </div> -->
<div class="border-t border-blue-gray-50 p-10"> <div class="p-4 flex items-center justify-between">
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> <p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than last Retirement goal</p>
week <h4
</p> class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
</div> $53k</h4>
</div> </div>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md"> <div class="border-t border-blue-gray-50 p-10">
<!-- <div --> <p
<!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-green-600 to-green-400 text-white shadow-green-500/40 shadow-lg absolute -mt-4 grid h-16 w-16 place-items-center"> --> class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" --> <strong class="text-green-500">Make this percent bar or graph or
<!-- class="w-6 h-6 text-white"> --> something
<!-- <path --> </strong>&nbsp;than lastweek
<!-- d="M6.25 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM3.25 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM19.75 7.5a.75.75 0 00-1.5 0v2.25H16a.75.75 0 000 1.5h2.25v2.25a.75.75 0 001.5 0v-2.25H22a.75.75 0 000-1.5h-2.25V7.5z"> --> </p>
<!-- </path> --> </div>
<!-- </svg> --> </div>
<!-- </div> --> <div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<!-- we can use this later if needed not sure what it shoudl be for right now. --> <div class="p-4 flex items-center justify-between">
<!-- <div class="p-4 text-right"> --> <p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
<!-- <p class="block antialiased font-sans text-sm leading-normal font-normal text-blue-gray-600">New Clients --> Retirement goal</p>
<!-- </p> --> <h4
<!-- <h4 --> class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
<!-- class="block antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> --> $53k</h4>
<!-- 3,462</h4> --> </div>
<!-- </div> --> <div class="border-t border-blue-gray-50 p-10">
<!-- <div class="border-t border-blue-gray-50 p-4"> --> <p
<!-- <p class="block antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> --> class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<!-- <strong class="text-red-500">-2%</strong>&nbsp;than yesterday --> <strong class="text-green-500">Make this percent bar or graph or
<!-- </p> --> something
<!-- </div> --> </strong>&nbsp;than last
<!-- </div> --> week
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md"> </p>
<div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md"> </div>
<div class="p-4 flex items-center justify-between"> </div>
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600"> <div class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
Retirement goal</p> <!-- <div -->
<h4 <!-- class="bg-clip-border mx-4 rounded-xl overflow-hidden bg-gradient-to-tr from-green-600 to-green-400 text-white shadow-green-500/40 shadow-lg absolute -mt-4 grid h-16 w-16 place-items-center"> -->
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> <!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" -->
$53k</h4> <!-- class="w-6 h-6 text-white"> -->
</div> <!-- <path -->
<div class="border-t border-blue-gray-50 p-10"> <!-- d="M6.25 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM3.25 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM19.75 7.5a.75.75 0 00-1.5 0v2.25H16a.75.75 0 000 1.5h2.25v2.25a.75.75 0 001.5 0v-2.25H22a.75.75 0 000-1.5h-2.25V7.5z"> -->
<p class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> <!-- </path> -->
<strong class="text-green-500">Make this percent bar or graph or something </strong>&nbsp;than last <!-- </svg> -->
week <!-- </div> -->
</p> <!-- we can use this later if needed not sure what it shoudl be for right now. -->
</div> <!-- <div class="p-4 text-right"> -->
</div> <!-- <p class="block antialiased font-sans text-sm leading-normal font-normal text-blue-gray-600">New Clients -->
</div> <!-- </p> -->
</div> <!-- <h4 -->
</div> <!-- class="block antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900"> -->
<div class="text-blue-gray-600"> <!-- 3,462</h4> -->
</div> <!-- </div> -->
</main> <!-- <div class="border-t border-blue-gray-50 p-4"> -->
<!-- <p class="block antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600"> -->
<!-- <strong class="text-red-500">-2%</strong>&nbsp;than yesterday -->
<!-- </p> -->
<!-- </div> -->
<!-- </div> -->
<div
class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div
class="relative flex flex-col bg-clip-border rounded-xl bg-white text-gray-700 shadow-md">
<div class="p-4 flex items-center justify-between">
<p class="font-sans text-2xl leading-normal font-normal text-blue-gray-600">
Retirement goal</p>
<h4
class="block text-right antialiased tracking-normal font-sans text-2xl font-semibold leading-snug text-blue-gray-900">
$53k</h4>
</div>
<div class="border-t border-blue-gray-50 p-10">
<p
class=" antialiased font-sans text-base leading-relaxed font-normal text-blue-gray-600">
<strong class="text-green-500">Make this percent bar or
graph or something
</strong>&nbsp;than last
week
</p>
</div>
</div>
</div>
</div>
</div>
<div class="text-blue-gray-600">
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script> <%- include("./partials/navBar") %>
<script src="/static/scripts/geolocation.js"></script> <%- include("./partials/scriptLoader") %>
<!-- <script src="/static/scripts/calcExchange.js"></script> --> <%- include("./partials/footer") %>
<script>
// If no geoData provided thorugh EJS, send update request to backend
let country = "<%= geoData.country %>";
if (!country) getLocation();
</script>
<%- include("./partials/navBar") %>
<%- include("./partials/footer") %>
+38
View File
@@ -0,0 +1,38 @@
<%- include("./partials/fileHeader") %>
<main>
<div class="flex justify-center items-center h-screen">
<form action="/auth/resetPass" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Reset Password</h1>
<hr class="mt-3">
<% if (!reset ) { %>
<div class="mt-3">
<label for="email" class="block text-base mb-2">Email</label>
<input type="text" id="email" name="email"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Email" />
</div>
<% } else { %>
<div class="mt-3">
<h1 class="text-2xl block text-center font-bold text-red-600"> Reset link sent. </h1>
</div>
<% } %>
<!---->
<div class="text-red-800 font-bold" id="forgor">
<%= error%>
</div>
<% if (!reset) { %>
<div class="mt-5">
<button type="submit"
class="cursor-pointer border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Send
reset Link</button>
</div>
<% } %>
</div>
</form>
</div>
</main>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+1 -1
View File
@@ -25,7 +25,7 @@
<label>Remember me</label> <label>Remember me</label>
</div> </div>
<div> <div>
<a href="#" class="text-indigo-600 font-semibold">Forgot Password? </a> <a href="/forgotPassword" class="text-indigo-600 font-semibold">Forgot Password? </a>
</div> </div>
</div> </div>
<div class="mt-5"> <div class="mt-5">
+1
View File
@@ -6,5 +6,6 @@
</main> </main>
<%- include("./partials/navBar") %> <%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+2
View File
@@ -37,4 +37,6 @@
</div> </div>
</main> </main>
<%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+44
View File
@@ -1,3 +1,16 @@
<div class="fixed end-6 bottom-20 group">
<div id="factMenu" class="absolute right-0 bottom-full mb-2 hidden">
<div id="factContainer" class="bg-white p-4 rounded-lg shadow-md dark:bg-gray-700 w-64">
<p class="mb-2 text-sm text-gray-700 dark:text-gray-300">Insert the kind of short fact you would like to know more about:</p>
<input type="text" id="factInput" placeholder="Enter your topic here" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white dark:placeholder-gray-400">
<button type="button" id="factSubmitButton" class="mt-3 w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-700">Submit</button>
</div>
</div>
<button type="button" id="factButton" class="flex items-center justify-center text-white bg-blue-700 rounded-full w-14 h-14 hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 focus:outline-none dark:focus:ring-blue-800">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
<span class="sr-only">Get a Fact</span>
</button>
</div>
<footer class="mt-16"> <footer class="mt-16">
<div class="fixed bottom-0 left-0 z-50 hidden lg:block w-full h-16 bg-gray-800 border-t border-gray-700 p-6 text-center text-gray-400 text-sm"> <div class="fixed bottom-0 left-0 z-50 hidden lg:block w-full h-16 bg-gray-800 border-t border-gray-700 p-6 text-center text-gray-400 text-sm">
@@ -6,5 +19,36 @@
</div> </div>
</div> </div>
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
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');
});
});
</script>
</body> </body>
</html> </html>
+5 -3
View File
@@ -17,12 +17,12 @@
</div> </div>
<div style="display: none;" class="flex items-center" id="currencyExchange"> <div style="display: none;" class="flex items-center" id="currencyExchange">
<div class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"> <div id="exFrom" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600">
<img src="" id="yourFlag" class="h-4 w-4 me-2" alt=""> <img src="" id="yourFlag" class="h-4 w-4 me-2" alt="">
<p id="flagTag"></p> <p id="flagTag"></p>
</div> </div>
<div class="relative w-full"> <div class="relative w-full">
<p id="exchange" class="block text-center p-2.5 w-30 z-20 text-sm text-gray-900 bg-gray-50 border-s-0 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500"> <p id="exchange" class="block text-center p-2.5 w-40 z-20 text-sm border bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-blue-500">
</p> </p>
</div> </div>
<button id="dropdown-country-button" value="" data-dropdown-toggle="dropdown-country" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-e-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600" type="button"> <button id="dropdown-country-button" value="" data-dropdown-toggle="dropdown-country" class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-e-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600" type="button">
@@ -38,6 +38,8 @@
</ul> </ul>
</div> </div>
</div> </div>
<% } else if(geoData.message == "error") { %>
<p>error</p>
<% } else { %> <% } else { %>
<div class="flex items-center"> <div class="flex items-center">
<div class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"> <div class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-900 bg-gray-100 border border-gray-300 rounded-s-lg focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600">
@@ -45,7 +47,7 @@
<p id="flagTag"></p> <p id="flagTag"></p>
</div> </div>
<div class="relative w-full"> <div class="relative w-full">
<p id="exchange" class="block text-center p-2.5 w-30 z-20 text-sm text-gray-900 bg-gray-50 border-s-0 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500"> <p id="exchange" class="block text-center p-2.5 w-40 z-20 text-sm text-gray-900 bg-gray-50 border-s-0 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500">
$1.00 = $<%= (1 * geoData.toCurrencyRates["USD"]).toFixed(2) %> $1.00 = $<%= (1 * geoData.toCurrencyRates["USD"]).toFixed(2) %>
</p> </p>
</div> </div>
+7
View File
@@ -0,0 +1,7 @@
<!-- Get geolocation if not in session and load conversion rates -->
<script src="/static/scripts/geolocation.js"></script>
<script>
// If no geoData provided thorugh EJS, send update request to backend
let country = "<%= geoData.country %>";
if (!country) getLocation();
</script>
+2 -1
View File
@@ -62,7 +62,7 @@
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600"> <div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
<p class="text-sm text-blue-700 dark:text-blue-300"> <p class="text-sm text-blue-700 dark:text-blue-300">
USE YOUR HEAD MOCK <%= suggestions %>
</p> </p>
</div> </div>
</div> </div>
@@ -71,4 +71,5 @@
</main> </main>
<%- include("./partials/navBar") %> <%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+2 -1
View File
@@ -10,7 +10,7 @@
<% plans.forEach(plan => { %> <% plans.forEach(plan => { %>
<a href="/plans/<%= plan._id %>" class="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"> <a href="/plans/<%= plan._id %>" class="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"><%= plan.name %></h5> <h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"><%= plan.name %></h5>
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-4 dark:bg-gray-700"> <div class="w-full bg-black rounded-full h-2.5 mb-4 dark:bg-black">
<div class="bg-green-600 h-2.5 rounded-full dark:bg-green-500" style="width: <%= plan.progress %>%;"></div> <div class="bg-green-600 h-2.5 rounded-full dark:bg-green-500" style="width: <%= plan.progress %>%;"></div>
</div> </div>
<p class="font-normal text-gray-700 dark:text-gray-400"><%= plan.description %></p> <p class="font-normal text-gray-700 dark:text-gray-400"><%= plan.description %></p>
@@ -21,4 +21,5 @@
</main> </main>
<%- include("./partials/navBar") %> <%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+2 -1
View File
@@ -56,7 +56,7 @@
<form action="/updateAccount" method="post" class="space-y-4 mt-4" id="account-form"> <form action="/updateAccount" method="post" class="space-y-4 mt-4" id="account-form">
<label for="email" class="block text-sm font-medium text-gray-700">Email</label> <label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input id="email" disabled type="text" name="email" value="<%= user.email %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none"> <input id="email" disabled type="email" name="email" value="<%= user.email %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
<label for="name" class="block text-sm font-medium text-gray-700">Name</label> <label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<input id="name" disabled type="text" name="name" value="<%= user.name %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none"> <input id="name" disabled type="text" name="name" value="<%= user.name %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none">
@@ -138,4 +138,5 @@
<script src="/static/scripts/profile.js"></script> <script src="/static/scripts/profile.js"></script>
<%- include("./partials/navBar") %> <%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+2
View File
@@ -68,4 +68,6 @@
</div> </div>
</main> </main>
<%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>
+36
View File
@@ -0,0 +1,36 @@
<%- include("./partials/fileHeader") %>
<main>
<div class="flex justify-center items-center h-screen">
<form action="/resetLink" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Reset Password</h1>
<hr class="mt-3">
<input type="hidden" name="token" value="<%=token %>">
<div class="mt-3">
<label for="password" class="block text-base mb-2">New Password:</label>
<input type="password" id="password" name="password"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Password" />
</div>
<div class="mt-3">
<label for="password" class="block text-base mb-2">Confirm Password:</label>
<input type="password" id="Repassword" name="confirmPassword"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="ReType Password" />
</div>
<% if (typeof errMessage !=='undefined' ) { %>
<div class="text-red-800 font-bold" id="forgor">
<%= errMessage %>
</div>
<% } %>
<div class="mt-5">
<button type="submit"
class="cursor-pointer border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Change
Password</button>
</div>
</div>
</form>
</div>
</main>
<%- include("./partials/footer") %>
+43 -43
View File
@@ -1,46 +1,46 @@
<%- include("./partials/fileHeader") %> <%- include("./partials/fileHeader") %>
<%- include("./partials/headerStart") %> <%- include("./partials/headerStart") %>
<main> <main>
<div class="flex justify-center items-center h-screen"> <div class="flex justify-center items-center h-screen">
<form action="/signup" method="POST"> <form action="/signup" method="POST">
<div class="w-96 p-6 shadow-1g bg-white rounded-md"> <div class="w-96 p-6 shadow-1g bg-white rounded-md">
<h1 class="text-3xl block text-center font-semibold">Signup</h1> <h1 class="text-3xl block text-center font-semibold">Signup</h1>
<hr class="mt-3"> <hr class="mt-3">
<div class="mt-3"> <div class="mt-3">
<label for="name" class="block text-base mb-2">Name</label> <label for="name" class="block text-base mb-2">Name</label>
<input type="text" id="name" name="name" <input type="text" id="name" name="name"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 " class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Name" /> placeholder="Enter Name" />
</div> </div>
<div class="mt-3"> <div class="mt-3">
<label for="email" class="block text-base mb-2">Email</label> <label for="email" class="block text-base mb-2">Email</label>
<input type="text" id="email" name="email" <input type="email" id="email" name="email"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 " class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Email" /> placeholder="Enter Email" />
</div> </div>
<div class="mt-3"> <div class="mt-3">
<label for="password" class="block text-base mb-2">Password</label> <label for="password" class="block text-base mb-2">Password</label>
<input type="password" id="password" name="password" <input type="password" id="password" name="password"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 " class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Password" /> placeholder="Enter Password" />
<label for="password" class="block text-base mb-2 mt-3">Re-type Password</label> <label for="password" class="block text-base mb-2 mt-3">Re-type Password</label>
<input type="password" id="repassword" name="repassword" <input type="password" id="repassword" name="repassword"
class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 " class="border focus:border-gray-600 w-full rounded-md text-base px-2 py-1 focus:outline-none focus:ring-0 "
placeholder="Enter Password" /> placeholder="Enter Password" />
</div> </div>
<div class="text-red-800 font-bold " id="forgor"> <%= errMessage %></div> <div class="text-red-800 font-bold " id="forgor">
<div class="mt-5"> <%= errMessage %>
<button type="submit" </div>
class="border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Signup</button> <div class="mt-5">
</div> <button type="submit"
<div class="mt-5 block justify-between items-center"> class="border-2 border-gray-600 bg-blue-600 text-white py-1 w-full rounded-md hover:bg-transparent hover:text-indigo-700 font-semibold">Signup</button>
<label>Already have an account?</label> </div>
<a href="/login" class="text-indigo-600 font-semibold">Login</a> <div class="mt-5 block justify-between items-center">
</div> <label>Already have an account?</label>
</div> <a href="/login" class="text-indigo-600 font-semibold">Login</a>
</form> </div>
</div> </div>
</main> </main>
<%- include("./partials/footer") %> <%- include("./partials/footer") %>