654 lines
18 KiB
JavaScript
654 lines
18 KiB
JavaScript
import dotenv from "dotenv";
|
||
dotenv.config()
|
||
|
||
|
||
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 apiRoutes from './routes/index.js'
|
||
|
||
|
||
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
|
||
import expressLayouts from "express-ejs-layouts";
|
||
import jwt from "jsonwebtoken";
|
||
|
||
const app = express();
|
||
|
||
|
||
const server = new createServer(app);
|
||
const io = new Server(server);
|
||
|
||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||
console.log("⚠️ Certificate error:", url);
|
||
|
||
// Allow for ngrok only
|
||
if (url.includes("ngrok")) {
|
||
event.preventDefault();
|
||
callback(true); // approve the certificate
|
||
} else {
|
||
callback(false); // reject others
|
||
}
|
||
});
|
||
|
||
app.set("view engine", "ejs");
|
||
|
||
app.use(expressLayouts);
|
||
|
||
// DEFAULT layout for all pages
|
||
app.set("layout", "layouts/dashboard");
|
||
const PORT = process.env.PORT || 8000;
|
||
|
||
app.use(cors({
|
||
sameSite: "None"
|
||
}))
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
app.use("/public", express.static(path.join(__dirname, "..", "public")));
|
||
|
||
app.use(bodyParser.json({ limit: '40mb' }));
|
||
app.use(bodyParser.urlencoded({ extended: true, limit: '40mb' }));
|
||
app.use(bodyParser.text());
|
||
|
||
// API Route
|
||
app.use('/api', apiRoutes);
|
||
|
||
app.get("/download-tracker", (req, res) => {
|
||
res.render("download_tracker", {
|
||
title: 'Download tracker',
|
||
version: "3.2.1",
|
||
layout: 'layouts/main'
|
||
});
|
||
});
|
||
|
||
app.get("/download-file", (req, res) => {
|
||
const os = req.query.os || "windows";
|
||
|
||
if (os === "windows") {
|
||
return res.download("./files/docTracker-3.2.1.exe");
|
||
} else {
|
||
return res.download("./files/docTracker-3.2.1.dmg");
|
||
}
|
||
});
|
||
|
||
app.get('/varegister/:sf_token?', (req, res) => {
|
||
const { sf_token } = req.params;
|
||
|
||
// No token → invalid request
|
||
if (!sf_token) {
|
||
return res.status(404).render('not_found', {
|
||
title: 'Page Not Found',
|
||
layout: 'layouts/error'
|
||
});
|
||
}
|
||
|
||
try {
|
||
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
||
|
||
// Token valid
|
||
return res.render('va_register', {
|
||
title: 'VA Registration',
|
||
email: decoded.email,
|
||
layout: 'layouts/varegisterlayout'
|
||
});
|
||
|
||
} catch (error) {
|
||
// Invalid / expired token
|
||
return res.status(401).render('not_found', {
|
||
title: 'Invalid or Expired Link',
|
||
layout: 'layouts/error'
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
app.get('/clientregister/:sf_token?', (req, res) => {
|
||
const { sf_token } = req.params;
|
||
|
||
// No token → invalid request
|
||
if (!sf_token) {
|
||
return res.status(404).render('not_found', {
|
||
title: 'Page Not Found',
|
||
layout: 'layouts/error'
|
||
});
|
||
}
|
||
|
||
try {
|
||
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
||
|
||
// Token valid
|
||
return res.render('va_client_register', {
|
||
title: 'Client Registration',
|
||
email: decoded.email,
|
||
layout: 'layouts/vaclientlayout'
|
||
});
|
||
|
||
} catch (error) {
|
||
// Invalid / expired token
|
||
return res.status(401).render('not_found', {
|
||
title: 'Invalid or Expired Link',
|
||
layout: 'layouts/error'
|
||
});
|
||
}
|
||
});
|
||
|
||
app.get('/auth/password/reset-form', render_reset_form);
|
||
|
||
app.get("/client/login", (req, res) => {
|
||
// res.render("client_login")
|
||
res.render("client/client_login", {
|
||
layout: "layouts/auth", // ⬅️ override layout
|
||
title: "DocVA | Login"
|
||
});
|
||
})
|
||
|
||
app.get("/client/forgot-password", (req, res) => {
|
||
|
||
res.render("forgot_password", {
|
||
title: "Forgot Password",
|
||
layout: "layouts/forgotpasswordlayout"
|
||
});
|
||
})
|
||
|
||
// Client Dashboard Routes with new file names
|
||
app.get('/client/dashboard', async (req, res) => {
|
||
try {
|
||
// const dashboardData = await dashboardService.getClientDashboard(req.user.id);
|
||
// res.render('client/dashboard-dashboard', dashboardData);
|
||
// res.render('client/dashboard-dashboard');
|
||
res.render('client/dashboard-dashboard', {
|
||
title: "Client Dashboard",
|
||
pageScript: "dashboard.js"
|
||
});
|
||
} catch (error) {
|
||
console.error('Dashboard error:', error);
|
||
res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' });
|
||
}
|
||
});
|
||
|
||
app.get('/client/virtual-assistants', async (req, res) => {
|
||
try {
|
||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||
// res.render('client/dashboard-virtual-assistants');
|
||
res.render('client/dashboard-virtual-assistants', {
|
||
title: "Virtual Assistants",
|
||
pageScript:"dashboard-virtual-assistants.js"
|
||
|
||
});
|
||
// res.render('client/dashboard-virtual-assistants', vaData);
|
||
} catch (error) {
|
||
console.error('Virtual assistants error:', error);
|
||
res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' });
|
||
}
|
||
});
|
||
|
||
app.get('/client/projects', async (req, res) => {
|
||
try {
|
||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||
// res.render('client/dashboard-projects');
|
||
res.render('client/dashboard-projects', {
|
||
title: "Projects",
|
||
pageScript:"projects.js"
|
||
|
||
});
|
||
} catch (error) {
|
||
console.error('Projects error:', error);
|
||
res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' });
|
||
}
|
||
});
|
||
|
||
app.get('/client/profile', async (req, res) => {
|
||
try {
|
||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||
// res.render('client/dashboard-profile');
|
||
res.render('client/dashboard-profile',{
|
||
title: "Profile",
|
||
pageScript:"profile.js"
|
||
});
|
||
} catch (error) {
|
||
console.error('Projects error:', error);
|
||
res.status(500).render('client/dashboard-profile', { error: 'Failed to load profile' });
|
||
}
|
||
});
|
||
|
||
app.get('/client/virtual-assistants/manage-tips', async (req, res) => {
|
||
try {
|
||
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
|
||
// res.render('client/dashboard-manage-va-tips');
|
||
res.render('client/dashboard-manage-va-tips', {
|
||
title: 'Manage VA Tips',
|
||
pageScript: 'manage-tips.js'
|
||
});
|
||
} catch (error) {
|
||
console.error('Projects error:', error);
|
||
res.status(500).render('client/dashboard-manage-va-tips', { error: 'Failed to load profile' });
|
||
}
|
||
});
|
||
|
||
app.get('/client/virtual-assistants/view-logs', async (req, res) => {
|
||
try {
|
||
// res.render('client/dashboard-va-logs');
|
||
res.render('client/dashboard-va-logs', {
|
||
title: 'VA Logs',
|
||
pageScript: 'va-logs.js'
|
||
});
|
||
} catch (error) {
|
||
console.error('Projects error:', error);
|
||
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
|
||
}
|
||
});
|
||
|
||
app.get('/client/credits', async (req, res) => {
|
||
try {
|
||
// res.render('client/dashboard-va-logs');
|
||
res.render('client/dashboard-credits', {
|
||
title: 'Credits',
|
||
pageScript: 'va-credits.js'
|
||
});
|
||
} catch (error) {
|
||
console.error('Projects error:', error);
|
||
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
|
||
}
|
||
});
|
||
|
||
|
||
app.get("/live/:projectId/:vaId", async (req, res) => {
|
||
const { projectId, vaId } = req.params;
|
||
|
||
if (!projectId || !vaId) {
|
||
return res.status(400).render("not_found", { title: "Page not found" });
|
||
}
|
||
|
||
let clientId = null;
|
||
try {
|
||
const clientRes = await ProjectModel.findById(projectId);
|
||
if (clientRes) {
|
||
clientId = clientRes.clientId.toString();
|
||
}
|
||
} catch (err) {
|
||
console.error("Error fetching client for project:", err.message);
|
||
return res.status(500).render("not_found", { title: "Server Error" });
|
||
}
|
||
|
||
if (!clientId) {
|
||
return res.status(400).render("not_found", { title: "Page not found" });
|
||
}
|
||
|
||
res.render("va_live_tracker", {
|
||
title: "VA Live Tracker",
|
||
projectId,
|
||
vaId,
|
||
clientId,
|
||
layout: "layouts/valivetrackerlayout" // ✅ ADD THIS
|
||
});
|
||
});
|
||
|
||
|
||
// app.get("/live/:projectId/:vaId", async (req, res) => {
|
||
// const { projectId, vaId } = req.params;
|
||
|
||
// if (!projectId || !vaId) {
|
||
// return res.status(400).send({ "message": "Page not found" });
|
||
// }
|
||
|
||
// let clientId = null;
|
||
// try {
|
||
// const clientRes = await ProjectModel.findById(projectId);
|
||
// if (clientRes) {
|
||
// clientId = clientRes.clientId.toString();
|
||
// }
|
||
// } catch (err) {
|
||
// console.error("Error fetching client for project:", err.message);
|
||
// return;
|
||
// }
|
||
|
||
// if (!clientId) return res.status(400).send({ "message": "Page not found" });
|
||
|
||
// res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
|
||
// })
|
||
|
||
// Handling Socket
|
||
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
|
||
|
||
const vaSocketRespectToClient = new Map();
|
||
|
||
// Handling Socket
|
||
io.use(async (socket, next) => {
|
||
try {
|
||
const token = socket.handshake.auth?.token;
|
||
const projectId = socket.handshake.auth?.projectId;
|
||
const userType = socket.handshake.auth?.userType;
|
||
const vaId = socket.handshake.auth?.vaId;
|
||
|
||
|
||
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()}-${vaUser._id.toString()}-${projectId.toString()}-va`, 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();
|
||
socket.data.vaId = vaId.toString();
|
||
socket.data.projectId = projectId;
|
||
|
||
// Get all VA IDs assigned to this client
|
||
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
|
||
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaId.toString()}-${projectId.toString()}-client`, socket.id)
|
||
|
||
// 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, projectId = null, clientId = null }) => {
|
||
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 = clientId;
|
||
|
||
// 🧩 Get allowed VAs for this client
|
||
const client = await ClientModel.findById(clientId);
|
||
|
||
const a = JSON.stringify(client);
|
||
console.log(a, "client data for allowed VAs");
|
||
if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
|
||
const projectVAIds = new Set();
|
||
|
||
const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
|
||
populated.vaAssigned.forEach((va) => projectVAIds.add(va.vaId.toString()));
|
||
|
||
socket.data.allowedVAs = Array.from(projectVAIds);
|
||
console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
|
||
|
||
io.to(vaSocketRespectToClient.get(`${clientId}-${id}-${projectId}-va`)).emit("startSendAppsDetails", { projId: projectId });
|
||
}
|
||
|
||
// 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 ${clientId} can view VAs:`, socket.data.allowedVAs);
|
||
}
|
||
});
|
||
|
||
// When VA sends live image
|
||
socket.on("sendImage", async ({ token, image, title, projectId }) => {
|
||
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 project = await ProjectModel.findById(projectId);
|
||
if (!project) {
|
||
console.log("❌ Invalid Project ID");
|
||
return;
|
||
}
|
||
|
||
const clientId = project.clientId.toString();
|
||
const vaId = vaUser._id.toString();
|
||
const clientVaUniqueId = `${clientId}-${vaId}-${projectId}-client`;
|
||
const socketId = vaSocketRespectToClient.get(clientVaUniqueId);
|
||
|
||
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);
|
||
// }
|
||
// }
|
||
|
||
// Can be like clientId-projectId-vaId to uniquely identify
|
||
socket.to(socketId).emit("receiveImage", image, title);
|
||
} catch (err) {
|
||
console.error("❌ Token verification failed:", err.message);
|
||
}
|
||
});
|
||
|
||
socket.on("selectApp", ({ appTitle, clientId, vaId, projectId }) => {
|
||
console.log(`VA selected app: ${appTitle}`);
|
||
|
||
console.log(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`), "vaSocketRespectToClient[clientId]")
|
||
// You can forward this info to the respective Electron client
|
||
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
|
||
});
|
||
|
||
socket.on("requestVisibleApps", async ({ token }) => {
|
||
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("captureAppsTracker");
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ Token verification failed:", err.message);
|
||
}
|
||
});
|
||
|
||
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();
|
||
|
||
// 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, vaId });
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error("❌ Token verification failed:", err.message);
|
||
}
|
||
});
|
||
|
||
|
||
// socket.on('disconnect', () => {
|
||
// console.log('a user Disconnected');
|
||
// });
|
||
|
||
// When client leaves or closes tab
|
||
socket.on("disconnect", () => {
|
||
if (socket.data.role === "client") {
|
||
const { clientId, vaId, projectId } = socket.data;
|
||
|
||
const vaSocketId = vaSocketRespectToClient.get(
|
||
`${clientId}-${vaId}-${projectId}-va`
|
||
);
|
||
|
||
if (vaSocketId) {
|
||
// io.to(vaSocketId).emit("stopSendAppsDetails");
|
||
io.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("stopSendAppsDetails", { projId: projectId });
|
||
}
|
||
}
|
||
|
||
|
||
});
|
||
|
||
});
|
||
|
||
app.get("/login", (req, res) => {
|
||
// res.render("client_login")
|
||
res.render("client/client_login", {
|
||
layout: "layouts/auth", // ⬅️ override layout
|
||
title: "DocVA | Login"
|
||
});
|
||
})
|
||
|
||
app.get("/forgot-password", (req, res) => {
|
||
res.render("forgot_password", {
|
||
title: "Forgot Password",
|
||
layout: "layouts/forgotpasswordlayout"
|
||
});
|
||
})
|
||
|
||
app.get("/health", (req, res) => {
|
||
return res.status(200).json({ "success": true, "message": "Server is Healthy" })
|
||
})
|
||
|
||
app.post("/post-test", (req, res) => {
|
||
console.log("✅ POST request received at /post-test:", req.body);
|
||
return res.status(200).json({ "success": true, "message": "POST request received", "data": req.body })
|
||
});
|
||
|
||
app.get("/tnyk/:sf_token", (req, res) => {
|
||
const sf_token = req.params.sf_token;
|
||
console.log("Received token:", sf_token);
|
||
|
||
try {
|
||
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
||
if (decoded) {
|
||
res.render('client/dashboard-dashboard', { sf_token, title: "Client" });
|
||
} else {
|
||
res.render('not_found', { title: "Client" });
|
||
}
|
||
} catch (error) {
|
||
res.render('not_found', { title: "Client" });
|
||
}
|
||
|
||
|
||
});
|
||
|
||
app.use((req, res) => {
|
||
res.status(404).render("not_found", {
|
||
layout: "layouts/error",
|
||
title: "404 - Page Not Found"
|
||
});
|
||
});
|
||
|
||
server.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}`);
|
||
}); |