diff --git a/package-lock.json b/package-lock.json index 0dfee86..ea79ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "puppeteer": "^24.10.1", "request": "^2.88.2", "require": "^2.4.20", + "socket.io": "^4.8.1", "uglify-js": "^3.19.3", "uuid": "^11.0.3", "winston": "^3.17.0", @@ -1976,12 +1977,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2022,7 +2038,6 @@ "version": "22.10.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -2729,6 +2744,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -3867,6 +3891,88 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "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/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -8127,6 +8233,137 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "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/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socks": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", @@ -8642,7 +8879,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/package.json b/package.json index b5a7865..846e8c2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "puppeteer": "^24.10.1", "request": "^2.88.2", "require": "^2.4.20", + "socket.io": "^4.8.1", "uglify-js": "^3.19.3", "uuid": "^11.0.3", "winston": "^3.17.0", diff --git a/src/controllers/va/vaLog/vaListLog.controller.js b/src/controllers/va/vaLog/vaListLog.controller.js index 770952d..f0249ed 100644 --- a/src/controllers/va/vaLog/vaListLog.controller.js +++ b/src/controllers/va/vaLog/vaListLog.controller.js @@ -3,34 +3,138 @@ import { VAModel, LoggedModel } from "../../../models/vaModel.js"; // Getting all active VA's and their details async function va_callback(req, res) { try { - // Find the latest log entry for each VA and return those whose last entry is LOGIN + // Find the latest log entry for each VA const agg = await LoggedModel.aggregate([ { $sort: { time: -1 } }, - { $group: { _id: "$vaId", lastLog: { $first: "$$ROOT" } } }, - { $match: { "lastLog.loggedType": "LOGIN" } } + { + $group: { + _id: "$vaId", + lastLog: { $first: "$$ROOT" }, + totalLogins: { + $sum: { + $cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0] + } + }, + lastActivity: { $first: "$time" } + } + }, + { + $match: { + "lastLog.loggedType": "LOGIN" + } + } ]); const vaIds = agg.map((a) => a._id).filter(Boolean); if (vaIds.length === 0) { - return res.status(200).json({ success: true, count: 0, data: [] }); + return res.status(200).json({ + success: true, + count: 0, + data: [], + message: "No active VAs found" + }); } + // Get VA details with additional fields + const vas = await VAModel.find({ _id: { $in: vaIds } }) + .select('userDetails.name userDetails.email userDetails.phone status createdAt') + .lean() + .exec(); - - const vas = await VAModel.find({ _id: { $in: vaIds } }).lean().exec(); - - // map lastLog by VA id for easy attachment - const lastLogByVa = agg.reduce((acc, a) => { - acc[String(a._id)] = a.lastLog; + // Create mapping for last log and activity data + const activityByVa = agg.reduce((acc, a) => { + acc[String(a._id)] = { + lastLog: a.lastLog, + totalLogins: a.totalLogins, + lastActivity: a.lastActivity + }; return acc; }, {}); - const results = vas.map((v) => ({ ...v, lastLog: lastLogByVa[String(v._id)] || null })); + // Calculate session duration for currently active VAs + const currentTime = new Date(); + const results = vas.map((v) => { + const activity = activityByVa[String(v._id)] || {}; + const lastLog = activity.lastLog; + + // Calculate current session duration if still logged in + let currentSessionDuration = null; + let isCurrentlyActive = false; + + if (lastLog && lastLog.loggedType === "LOGIN") { + const loginTime = new Date(lastLog.time); + currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); // in seconds + isCurrentlyActive = true; + + // Consider VA inactive if last login was more than 24 hours ago + const hoursSinceLastActivity = (currentTime - loginTime) / (1000 * 60 * 60); + if (hoursSinceLastActivity > 24) { + isCurrentlyActive = false; + } + } + + return { + id: v._id, + name: v.userDetails?.name || 'Unknown', + email: v.userDetails?.email, + phone: v.userDetails?.phone, + status: v.status || 'active', + isCurrentlyActive: isCurrentlyActive, + lastLoginTime: lastLog?.time || null, + lastActivity: activity.lastActivity, + currentSessionDuration: currentSessionDuration, + currentSessionFormatted: currentSessionDuration ? formatTime(currentSessionDuration) : null, + totalLogins: activity.totalLogins || 0, + lastProject: lastLog?.projectId || null, + vaSince: v.createdAt + }; + }); + + // Separate active and inactive VAs + const activeVAs = results.filter(va => va.isCurrentlyActive); + const inactiveButLoggedIn = results.filter(va => !va.isCurrentlyActive); + + return res.status(200).json({ + success: true, + count: { + total: results.length, + active: activeVAs.length, + inactive: inactiveButLoggedIn.length + }, + data: { + active: activeVAs, + inactive: inactiveButLoggedIn + }, + summary: { + currentlyWorking: activeVAs.length, + totalLoggedIn: results.length, + lastUpdated: currentTime.toISOString() + } + }); - return res.status(200).json({ success: true, count: results.length, data: results }); } catch (error) { - return res.status(500).send("Internal Server Error"); + console.error("VA callback error:", error); + return res.status(500).json({ + success: false, + message: "Internal Server Error", + error: error.message + }); + } +} + +// Helper function to format time +function formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + if (hours > 0) { + return `${hours}h ${minutes}m ${remainingSeconds}s`; + } else if (minutes > 0) { + return `${minutes}m ${remainingSeconds}s`; + } else { + return `${remainingSeconds}s`; } } diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js index e1d908e..97bd382 100644 --- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js +++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js @@ -4,8 +4,7 @@ import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../mod // Getting logged hours in a specific project of VA async function va_log_track_work_callback(req, res) { try { - const accessToken = req.headers.token; - + const accessToken = req.headers.token; if (!accessToken) { return res.status(401).json({ success: false, message: "Missing token" }); } diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js index b5229d3..66a78b3 100644 --- a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js +++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js @@ -29,23 +29,23 @@ async function va_profile_update_callback(req, res) { const payload = req.body || {}; const { userDetails } = payload; - + if (!userDetails) { return res.status(400).json({ success: false, message: "Missing userDetails" }); } // Allowed fields to update - const allowedFields = [ - "firstName", "lastName", "dob", "age", "address", - "bankName", "accountHolderName", "accountNumber", "routingNumber" - ]; + // const allowedFields = [ + // "firstName", "lastName", "dob", "age", "address", + // "bankName", "accountHolderName", "accountNumber", "routingNumber" + // ]; - // Update allowed fields only - allowedFields.forEach(field => { - if (userDetails[field] !== undefined) { - vaUser.userDetails[field] = userDetails[field]; - } - }); + // // Update allowed fields only + // allowedFields.forEach(field => { + // if (userDetails[field] !== undefined) { + // vaUser.userDetails[field] = userDetails[field]; + // } + // }); /* ---------------------- PASSWORD UPDATE ---------------------- */ if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) { @@ -78,28 +78,13 @@ async function va_profile_update_callback(req, res) { const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); vaUser.userDetails.password = hashedPassword; + } else { + return res.status(400).json({ + success: false, + message: "Data not found", + }); } - // if (userDetails.password) { - // if (!userDetails.cPassword) { - // return res.status(400).json({ - // success: false, - // message: "Please confirm your new password", - // }); - // } - - // if (userDetails.password !== userDetails.cPassword) { - // return res.status(400).json({ - // success: false, - // message: "Passwords do not match", - // }); - // } - - // const saltRounds = 10; - // const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds); - // vaUser.userDetails.password = hashedPassword; - // } - await vaUser.save(); const updatedUser = await VAModel.findById(vaUser._id) diff --git a/src/controllers/va/vaProjects/vaProjectLogList.controller.js b/src/controllers/va/vaProjects/vaProjectLogList.controller.js index be7b7fb..bd7e541 100644 --- a/src/controllers/va/vaProjects/vaProjectLogList.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLogList.controller.js @@ -52,7 +52,12 @@ async function va_projects_log_list_function(req, res) { projectList = [project]; } else { - projectList = await ProjectModel.find({ vaAssigned: vaId }); + const all = await ProjectModel.find().lean(); + console.log(all.map(p => p.vaAssigned)); + + console.log("VA ID:", vaId.toString()); + + projectList = await ProjectModel.find({ vaAssigned: vaUser._id }); } if (!projectList.length) { diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 0f1ea53..cb8a078 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -4,28 +4,39 @@ import logger from "../config/logger.js"; async function authValidator(req, res, next) { try { - const accessToken = req.headers.token; - if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" }); + const sf_token = req.header.sf_token; + if (sf_token) { - const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + } else { + const accessToken = req.headers.token; + if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" }); - if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) { - const vaEmail = decoded.email; - - const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }) - if (!vaUser) { - return res.status(401).json({ success: false, message: "Not Authorized" }); - } - if (!("accessToken" in vaUser) || !vaUser.accessToken) { - return res.status(401).json({ success: false, message: "Not Authorized" }); - } - - if (vaUser.accessToken === accessToken) { - req.body.vaId = vaUser._id - req.body.vaEmail = vaEmail - next(); - } else { - return res.status(401).json({ success: false, message: "Not Authorized" }); + // return res.status(200).json({ + // success: true, + // message: "Log Out Successfull", + // }); + + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) { + const vaEmail = decoded.email; + + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }) + if (!vaUser) { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + if (!("accessToken" in vaUser) || !vaUser.accessToken) { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + + if (vaUser.accessToken === accessToken) { + req.body.vaId = vaUser._id + req.body.vaEmail = vaEmail + next(); + } else { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + } } diff --git a/src/models/vaModel.js b/src/models/vaModel.js index aaf9fb3..3f6a423 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -80,6 +80,10 @@ const projectSchema = new Schema({ type: Number, default: 0 }, + clientId: { + type: Types.ObjectId, + ref: "Client" + }, vaAssigned: [ { type: Types.ObjectId, diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 59533cf..7beaf43 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -36,7 +36,7 @@ vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update -vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC) +vaRoutes.get("/:userName", va_username_callback); // VA Username Callback // timesheet adjustment routes vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date diff --git a/src/server.js b/src/server.js index 489708d..53d0c95 100644 --- a/src/server.js +++ b/src/server.js @@ -1,19 +1,25 @@ import dotenv from "dotenv"; dotenv.config() +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; import express from 'express'; +import { createServer } from 'http'; import mongoose from "mongoose"; import bodyParser from "body-parser"; +import { Server } from 'socket.io'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; +import fs from "node:fs" import apiRoutes from './routes/index.js' import { render_reset_form } from "./controllers/passwordReset.controllers.js"; const app = express(); +const server = new createServer(app); +const io = new Server(server); app.set("view engine", "ejs"); const PORT = process.env.PORT || 8000; @@ -47,9 +53,250 @@ app.get('/health', (req, res) => { res.status(201).send({ status: "Server is healthy" }); }) +app.get("/client/login", (req, res) => { + res.render("client_login") +}) +app.get("/client/forgot-password", (req, res) => { + res.render("forgot_password") +}) + +app.get("/live/:projectId", (req, res) => { + const { projectId } = req.params; + res.render("va_live_tracker", { title: "VA Live Tracker", projectId }) +}) + +// Handling Socket +import jwt from "jsonwebtoken"; +import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js'; +import { ObjectId } from 'mongodb/lib/bson.js'; + + +const vaSocketRespectToClient = new Map(); + +io.use(async (socket, next) => { + try { + const token = socket.handshake.auth?.token; + const projectId = socket.handshake.auth?.projectId; + const userType = socket.handshake.auth?.userType; + + console.log("🔐 Received token:", token); + + if (!token) { + return next(new Error("Not Authorized: Missing Token")); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (!decoded?.email) { + return next(new Error("Invalid token payload")); + } + + /* ---------------------- 1⃣ Check if VA ---------------------- */ + let vaUser = null; + let clientData = null; + + if (userType === "va") { + vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }); + clientData = await ProjectModel.findById(projectId).populate("clientId"); + } else if (userType === "client") { + clientData = await ProjectModel.findById(projectId).populate("clientId"); + } + + if (clientData == null) return next(new Error("Invalid Project ID")); + const vaProjClientId = clientData.clientId._id; + + if (vaUser && vaUser.accessToken === token) { + socket.data.role = "va"; + socket.data.vaId = vaUser._id.toString(); + socket.data.vaEmail = decoded.email; + + const a = vaProjClientId.toString(); + const b = clientData.clientId._id.toString(); + const c = JSON.stringify(clientData); + + console.log(a, b, c, "vaProjClientId vs clientData.clientId"); + + vaSocketRespectToClient.set(vaProjClientId.toString(), socket.id) + + console.log("✅ Authenticated VA:", vaUser.userDetails.email); + return next(); + } + + /* ---------------------- 2⃣ Check if Client ---------------------- */ + // const clientUser = await ClientModel.findOne({ "userDetails.email": decoded.email }); + + // if (clientUser) { + // socket.data.role = "client"; + // socket.data.clientId = clientUser._id.toString(); + + // // Get all VA IDs assigned to this client + // socket.data.allowedVAs = clientUser.vaIds.map((va) => va.toString()); + + // console.log("✅ Authenticated Client:", clientUser.userDetails.email); + // return next(); + // } + const clientUser = clientData.clientId; + + if (clientUser) { + socket.data.role = "client"; + socket.data.clientId = clientUser._id.toString(); + + // Get all VA IDs assigned to this client + socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString()); + + // console.log("✅ Authenticated Client:", clientUser.userDetails.email); + return next(); + } + + return next(new Error("Not Authorized: Invalid User")); + } catch (error) { + console.error("❌ Socket Auth Error:", error.message); + next(new Error("Authentication failed: " + error.message)); + } +}); + + + +io.on("connection", (socket) => { + console.log("User connected:", socket.id); + + // Register role and ID (client or VA) + socket.on("register", async ({ role, id }) => { + if (role === "va") { + socket.data.role = "va"; + socket.data.vaId = id; + console.log(`VA registered: ${id}`); + } + + if (role === "client") { + socket.data.role = "client"; + socket.data.clientId = id; + + // 🧩 Get allowed VAs for this client + const client = await ClientModel.findById(id) + .populate("projectIds") // to access their projects + .populate("vaIds"); // optional if you track directly + + // Collect all VA IDs from their projects + const projectVAIds = new Set(); + for (const project of client.projectIds) { + const populated = await ProjectModel.findById(project._id).populate("vaAssigned"); + populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString())); + } + + // Store allowed VA IDs in socket memory + socket.data.allowedVAs = Array.from(projectVAIds); + console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs); + } + }); + + // When VA sends live image + socket.on("sendImage", async ({ token, image, title }) => { + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + const vaEmail = decoded.email; + + // Find VA in DB + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + console.log("❌ Unauthorized VA"); + return; + } + + const vaId = vaUser._id.toString(); + console.log("🟢 Received image from VA:", vaId); + + // Broadcast image only to allowed clients + for (let [_, clientSocket] of io.sockets.sockets) { + if ( + clientSocket.data.role === "client" && + clientSocket.data.allowedVAs?.includes(vaId) + ) { + clientSocket.emit("receiveImage", image, title); + } + } + } catch (err) { + console.error("❌ Token verification failed:", err.message); + } + }); + + socket.on("selectApp", ({ appTitle, clientId }) => { + console.log(`VA selected app: ${appTitle}`); + + const sssrrrr = vaSocketRespectToClient.get(clientId); + console.log(vaSocketRespectToClient.get(clientId), sssrrrr, "vaSocketRespectToClient[clientId]") + // You can forward this info to the respective Electron client + socket.to(vaSocketRespectToClient.get(clientId)).emit("appToCapture", { appTitle }); + }); + + socket.on("getVisibleApps", async ({ token, apps }) => { + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + const vaEmail = decoded.email; + + // Find VA in DB + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + console.log("❌ Unauthorized VA"); + return; + } + + const vaId = vaUser._id.toString(); + console.log("🟢 Received Apps from VA:", vaId); + + // Broadcast image only to allowed clients + for (let [_, clientSocket] of io.sockets.sockets) { + if ( + clientSocket.data.role === "client" && + clientSocket.data.allowedVAs?.includes(vaId) + ) { + clientSocket.emit("receiveVisibleApps", apps); + } + } + } catch (err) { + console.error("❌ Token verification failed:", err.message); + } + }); + + + socket.on('disconnect', () => { + console.log('a user Disconnected'); + }); +}); + + + + +// io.on('connection', (socket) => { + +// socket.on("sendImage", (base64String) => { +// io.emit("receiveImage", base64String); +// }); + + +// socket.on('disconnect', () => { +// console.log('a user Disconnected'); +// }); +// }); + // Express App Listening -app.listen(PORT, () => { +// app.listen(PORT, () => { +// mongoose +// .connect( +// process.env.MONGO_URI, +// ) +// .then(() => console.log("mongodb connected")) +// .catch((err) => { +// console.log(err.message); +// process.exit(1); +// }); +// console.log(`Express started on http://localhost:${PORT}`); +// }); + +server.listen(PORT, () => { mongoose .connect( process.env.MONGO_URI, diff --git a/src/testApiVpnConnection.js b/src/testApiVpnConnection.js new file mode 100644 index 0000000..e985b2a --- /dev/null +++ b/src/testApiVpnConnection.js @@ -0,0 +1,32 @@ +// testVpnApi.js +import axios from "axios"; + +const apiUrl = "http://165.140.240.203:3000/api"; +const apiKey = "hellovpn123"; + +const possiblePaths = [ + "/", "/v1", "/servers", "/status", "/vpn", "/health", "/ping", + "/nodes", "/list", "/connect", "/config", "/create", "/createConfig", + "/check", "/organizations", "/users" +]; + +async function testPaths() { + for (const path of possiblePaths) { + try { + const res = await axios.get(`${apiUrl}${path}`, { + headers: { "x-api-key": apiKey }, + timeout: 3000 + }); + console.log(`✅ ${path} → ${res.status} ${res.statusText}`); + console.log("Response:", res.data); + } catch (err) { + if (err.response) { + console.log(`❌ ${path} → ${err.response.status}`); + } else { + console.log(`⚠️ ${path} → No response`); + } + } + } +} + +testPaths(); \ No newline at end of file diff --git a/views/client_login.ejs b/views/client_login.ejs new file mode 100644 index 0000000..399c259 --- /dev/null +++ b/views/client_login.ejs @@ -0,0 +1,297 @@ + + + +
+ + +