diff --git a/app.js b/app.js index e3f7e23..ff0b9c5 100644 --- a/app.js +++ b/app.js @@ -3,7 +3,7 @@ const MongoStore = require("connect-mongo"); const session = require("express-session"); const express = require('express'); const path = require('path'); -const joi = require('joi'); +const joi = require('joi'); require('dotenv').config(); @@ -12,7 +12,7 @@ const port = process.env.PORT || 3000; const mongoURI = process.env.mongoURI || "mongodb://localhost:27017/"; const database = process.env.database || "knoldus"; // Database name -const secret = process.env.secret || "123-secret-xyz"; +const secret = process.env.secret || "123-secret-xyz"; /*** Sessions ***/ app.use(session({ @@ -24,22 +24,25 @@ app.use(session({ })); app.set('view engine', 'ejs'); -app.set('views',path.join(__dirname, 'src/views')); +app.set('views', path.join(__dirname, 'src/views')); app.use(express.urlencoded({ extended: true })); app.use("/static", express.static("./src/public")); app.use("/images", express.static("./src/public/images")); +app.use(express.json()); /*** Database ***/ const { connectMongo, getCollection } = require("./src/database/connection"); let users; let assets; +let plans; async function initDatabase() { const db = await connectMongo(mongoURI, database); - + // For any collection, init here users = await getCollection(db, "users"); assets = await getCollection(db, "assets"); + plans = await getCollection(db, "plans"); } /*** ROUTINGS ***/ @@ -60,7 +63,7 @@ app.get('/signup', (req, res) => { app.get('/login', (req, res) => { if (req.session.authenticated) { res.redirect("/home"); - return res.status(status.Ok); + return res.status(status.Ok); } res.render('login', { errMessage: req.session.errMessage }); return res.status(status.Ok); @@ -71,6 +74,15 @@ app.get('/aboutUs', (req, res) => { return res.status(status.Ok); }); +app.post('/api/location', async (req,res) => { + const { latitude, longitude } = req.body; + + const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&result_type=country&key=${process.env.geolocation_api}`); + + const data = await response.json(); + + res.json(data); +}); // Initialize database and start app initDatabase().then(() => { @@ -81,16 +93,16 @@ initDatabase().then(() => { // Import middleware & apply to user routes const middleware = require("./src/auth/middleware")(users); - app.use(require('./src/router/user')(middleware, users, assets)); + app.use(require('./src/router/user')(middleware, users, plans, assets)); // 404 handler app.get('/*splat', (req, res) => { res.send('404 Not Found'); return res.status(status.NotFound); - }); + }); // Start app app.listen(port, () => { console.log(`Server listening on port ${port}`); - }); + }); }); diff --git a/package-lock.json b/package-lock.json old mode 100755 new mode 100644 index 54d9320..95750e3 --- a/package-lock.json +++ b/package-lock.json @@ -354,27 +354,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -549,6 +528,7 @@ "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -580,6 +560,7 @@ "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -969,13 +950,12 @@ } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-symbols": { @@ -1479,6 +1459,29 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -1976,16 +1979,15 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/tar": { diff --git a/src/auth/authentication.js b/src/auth/authentication.js index 0a25931..21ba26a 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -44,6 +44,7 @@ module.exports = (users) => { } req.session.authenticated = true; + req.session.userId = user._id; req.session.email = req.body.email; req.session.errMessage = ""; res.redirect("/home"); @@ -79,6 +80,7 @@ module.exports = (users) => { email: req.body.email, name: req.body.name, password: hashedPassword, + financialData: false, }).then((results, err) => { if (err) { console.error(err); @@ -88,6 +90,7 @@ module.exports = (users) => { req.session.authenticated = true; req.session.email = req.body.email; + req.session.userId = results.insertedId; req.session.errMessage = ""; return res.status(status.Ok).redirect("/home"); diff --git a/src/public/scripts/geolocation.js b/src/public/scripts/geolocation.js new file mode 100644 index 0000000..ebe001b --- /dev/null +++ b/src/public/scripts/geolocation.js @@ -0,0 +1,24 @@ +function getLocation() { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(success, error); + } +} + +function success(position) { + const { latitude, longitude } = position.coords; + + fetch("api/location" , { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ latitude, longitude }) + }).then(res => res.json()) + .then(data => { + console.log(data.results[0].formatted_address); + }) +} + +function error(err) { + console.error("Geolocation error: ", err); +} diff --git a/src/router/user.js b/src/router/user.js index 7a8725f..00cf478 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -2,6 +2,7 @@ const status = require("../util/statuses"); const ObjectId = require('mongodb').ObjectId; const bcrypt = require("bcrypt"); const joi = require("joi"); +const { ObjectId } = require('mongodb'); const salt = 12; /** @@ -58,13 +59,13 @@ const getAssetSchema = (type) => { * @param {MongoClient.collection} assets db collection * @returns {express.Router} user protected routes router */ -module.exports = (middleware, users, assets) => { +module.exports = (middleware, users, plans, assets) => { const router = require("express").Router(); router.use(middleware); router.get('/home', async (req, res) => { - res.render('home', { user: req.session.user }); + res.render('dashboard', { user: req.session.user }); return res.status(status.Ok); }); @@ -74,11 +75,119 @@ module.exports = (middleware, users, assets) => { return res.status(status.Ok); }); - router.get('/plans', (req, res) => { - res.render('plans', { user: req.session.user }); - return res.status(status.Ok); + router.get('/plans', async (req, res) => { + if (!req.session.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + + try { + + // console.log(req.user.email); + const userPlans = await plans.find({userEmail: req.user.email }).toArray(); + // console.log(userPlans); + res.render('plans', { + user: req.user, + plans: userPlans + }); + + } catch (err) { + console.error("Error fetching plans:", err); + req.session.errMessage = "Could not load your plans. Please try again."; + res.status(status.InternalServerError).redirect('/home'); + } }); + router.get('/plans/:id', async (req, res) => { + if (!req.session.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + + try { + const planId = req.params.id; + + + if (!ObjectId.isValid(planId)) { + req.session.errMessage = "Invalid plan ID format."; + return res.status(status.BadRequest).redirect('/plans'); + } + + const plan = await plans.findOne({ userEmail: req.user.email, _id: new ObjectId(planId) }); + + if (!plan) { + console.log(`Plan not found with ID: ${planId} for user: ${req.user.email}`); + req.session.errMessage = "Plan not found or you do not have permission to view it."; + return res.status(status.NotFound).redirect('/plans'); + } + // console.log("Found plan:", plan); + res.render('planDetail', { + user: req.user, + plan: plan + }); + + } catch (err) { + console.error("Error fetching plan:", err); + req.session.errMessage = "Could not load your plan. Please try again."; + res.status(status.InternalServerError).redirect('/home'); + } + }); + + router.get('/newPlan', (req, res) => { + if (!req.session.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + if(!req.user.financialData){ + req.session.errMessage = "Please complete your financial data before creating a plan."; + return res.status(status.Unauthorized).redirect('/questionnaire'); + } + const errMessage = req.session.errMessage; + req.session.errMessage = ""; + res.render('newPlan', { user: req.user, errMessage: errMessage }); + }); + + router.post('/newPlan', async (req, res) => { + if (!req.session.email) { + return res.status(status.Unauthorized).redirect('/login'); + } + + const planSchema = joi.object({ + name: joi.string().min(3).max(100).required(), + retirementAge: joi.number().min(18).max(120).required(), + retirementExpenses: joi.number().min(0).required(), + retirementAssets: joi.number().min(0).required(), + retirementLiabilities: joi.number().min(0).required(), + }); + + const validationOptions = { convert: true, abortEarly: false }; + const { error, value } = planSchema.validate(req.body, validationOptions); + + if (error) { + console.error("Plan validation error:", error.details); + req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', '); + res.status(status.BadRequest).redirect("/newPlan"); + return; + } + const newPlan = { + userEmail: req.user.email, + name: value.name, + retirementAge: value.retirementAge, + retirementExpenses: value.retirementExpenses, + retirementAssets: value.retirementAssets, + retirementLiabilities: value.retirementLiabilities, + progress: "0%" + }; + + try{ + await plans.insertOne(newPlan); + req.session.errMessage = ""; + res.redirect('/plans'); + } + catch(err){ + console.error("Error saving plan:", err); + req.session.errMessage = "An error occurred while saving your plan. Please try again."; + res.status(status.InternalServerError).redirect("/newPlan"); + } + }); + router.get('/more', (req, res) => { res.render('more', { user: req.session.user }); return res.status(status.Ok); @@ -101,6 +210,8 @@ module.exports = (middleware, users, assets) => { }); router.post('/questionnaire', (req, res) => { + // console.log("Questionnaire POST body:", req.body); + const questionnaireSchema = joi.object({ dob: joi.date().required(), education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(), @@ -109,10 +220,6 @@ module.exports = (middleware, users, assets) => { expenses: joi.number().min(0).required(), assets: joi.number().min(0).required(), liabilities: joi.number().min(0).required(), - retirementAge: joi.number().min(18).max(120).required(), - retirementExpenses: joi.number().min(0).required(), - retirementAssets: joi.number().min(0).required(), - retirementLiabilities: joi.number().min(0).required(), }); const validationOptions = { convert: true, abortEarly: false }; @@ -137,10 +244,6 @@ module.exports = (middleware, users, assets) => { expenses: value.expenses, assets: value.assets, liabilities: value.liabilities, - retirementAge: value.retirementAge, - retirementExpenses: value.retirementExpenses, - retirementAssets: value.retirementAssets, - retirementLiabilities: value.retirementLiabilities, } } ).then((result) => { diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs new file mode 100644 index 0000000..e4c9b36 --- /dev/null +++ b/src/views/dashboard.ejs @@ -0,0 +1,100 @@ +<%- include("./partials/fileHeader") %> + <%- include("./partials/header") %> + +
+

Welcome: <%= user.name %>

+ +
+ + + Create new plan + +
+ +
+
+ +
+ + + +
+

+ Retirement goal

+

+ $53k

+
+
+

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

+
+
+
+
+

+ Retirement goal

+

+ $53k

+
+
+

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

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

+ Retirement goal

+

+ $53k

+
+
+

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

+
+
+
+
+
+
+
+
+ + <%- include("./partials/navBar") %> + <%- include("./partials/footer") %> diff --git a/src/views/home.ejs b/src/views/home.ejs index 3637163..405e232 100644 --- a/src/views/home.ejs +++ b/src/views/home.ejs @@ -2,7 +2,6 @@ <%- include("./partials/header") %>
- <%- include("./partials/header") %> Welcome: <%= user.email %> The Dashboard Page <% if(user.authenticated) { %> diff --git a/src/views/index.ejs b/src/views/index.ejs deleted file mode 100644 index 935b4a0..0000000 --- a/src/views/index.ejs +++ /dev/null @@ -1,9 +0,0 @@ -<%- include("./partials/fileHeader") %> -<%- include("./partials/header") %> - -
- The Index Page -
- -<%- include("./partials/navBar") %> -<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/login.ejs b/src/views/login.ejs index 6bea42c..c8dbcbd 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -4,18 +4,18 @@
-

Login

+

Login


<%= errMessage %>
diff --git a/src/views/newPlan.ejs b/src/views/newPlan.ejs new file mode 100644 index 0000000..a05dd1a --- /dev/null +++ b/src/views/newPlan.ejs @@ -0,0 +1,40 @@ +<%- include("./partials/fileHeader") %> +<%- include("./partials/header") %> + +
+

Welcome: <%= user.name %>

+
+

Retirement Plan

+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ +<%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index 8f457f3..fabc132 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -1,10 +1,11 @@ -
+ +<%- include("./partials/navBar") %> +<%- include("./partials/footer") %> diff --git a/src/views/plans.ejs b/src/views/plans.ejs index 7f6e699..ce7ca80 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -1,8 +1,23 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/header") %> -
- The Plans Page +
+

Welcome: <%= user.name %>

+
+
<%- include("./partials/navBar") %> diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs index 10a1b29..993a8c1 100644 --- a/src/views/questionnaire.ejs +++ b/src/views/questionnaire.ejs @@ -2,7 +2,7 @@ <%- include("./partials/header") %>
-

Welcome: <%= user.email %>

+

Welcome: <%= user.name %>

Financial Questionnaire

@@ -60,26 +60,6 @@
- -
- - -
- -
- - -
- -
- - -
- -
- - -
diff --git a/src/views/signup.ejs b/src/views/signup.ejs index 97f1347..7180cb5 100644 --- a/src/views/signup.ejs +++ b/src/views/signup.ejs @@ -5,28 +5,28 @@
-

Signup

+

Signup


-
- - -
+
+ + +
<%= errMessage %>