diff --git a/package-lock.json b/package-lock.json index a0c011d..a773578 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,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", @@ -1975,12 +1976,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", @@ -2021,7 +2037,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" @@ -2728,6 +2743,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", @@ -3866,6 +3890,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.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", @@ -8117,6 +8223,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", @@ -8632,7 +8869,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 81f6408..4187f27 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,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/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 0f1ea53..3c6e886 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -6,7 +6,10 @@ async function authValidator(req, res, next) { try { const accessToken = req.headers.token; if (!accessToken) 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)) { diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 15e9105..8c32d0d 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -65,6 +65,10 @@ const projectSchema = new Schema({ type: Number, default: 0 }, + clientId: { + type: Types.ObjectId, + ref: "Client" + }, vaAssigned: [ { type: Types.ObjectId, @@ -204,7 +208,7 @@ const vaWorkTrackSchema = new Schema({ default: 0, }, }, - { timestamps: true } + { timestamps: true } ) /* ---------------------- Model Exports ---------------------- */ diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 7e1398f..72076b4 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -43,9 +43,9 @@ vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callb // Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA -vaRoutes.get("/list/project/:projectId/:from?/:to?", va_projects_log_list_function); +vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function); // GET Windows Tab Logged Data for VA IN Between Login and LOGOUT -vaRoutes.get("/report/project/:projectId/:from?/:to?", va_projects_report_function); +vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function); vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged IN / Logged Out in project @@ -56,8 +56,4 @@ vaRoutes.get("/log/workReport/", authValidator, va_log_work_report_callback); / vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callback); // VA / Project Audit Report // VA filter search: supports query params `name`, `email`, `from`, `to` - - - - export default vaRoutes; \ No newline at end of file diff --git a/src/server.js b/src/server.js index f71f4ed..e5016b5 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,10 @@ import express from 'express'; +import { createServer } from 'http'; + import mongoose from "mongoose"; import bodyParser from "body-parser"; +import { Server } from 'socket.io'; import dotenv from "dotenv"; import cors from 'cors'; @@ -9,10 +12,18 @@ import path from 'path'; import { fileURLToPath } from 'url'; +import fs from "node:fs" + + import apiRoutes from './routes/index.js' dotenv.config() + const app = express(); + +const server = new createServer(app); +const io = new Server(server); + app.set("view engine", "ejs"); const PORT = process.env.PORT || 8000; @@ -40,9 +51,212 @@ app.get('/health', (req, res) => { res.status(201).send({ status: "Server is healthy" }); }) +app.get("/live", (req, res) => { + res.render("va_live_tracker") +}) + +// Handling Socket +import jwt from "jsonwebtoken"; +import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js'; + + +const vaSocketRespectToClient = new Map(); + +io.use(async (socket, next) => { + try { + const token = socket.handshake.auth?.token; + const projectId = socket.handshake.auth?.projectId; + + 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 ---------------------- */ + const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }); + const clientData = await ClientModel.findOne({ "projectIds": projectId }); + const vaProjClientId = clientData._id; + + if (vaUser && vaUser.accessToken === token) { + socket.data.role = "va"; + socket.data.vaId = vaUser._id.toString(); + socket.data.vaEmail = decoded.email; + + vaSocketRespectToClient.set(vaProjClientId, 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(); + } + + 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}`); + + // You can forward this info to the respective Electron client + socket.to(vaSocketRespectToClient[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..2a6991f --- /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(); diff --git a/views/va_live_tracker.ejs b/views/va_live_tracker.ejs new file mode 100644 index 0000000..dc1eb4d --- /dev/null +++ b/views/va_live_tracker.ejs @@ -0,0 +1,141 @@ + + + +
+ +