diff --git a/public/js/va-register.js b/public/js/va-register.js index 5f73dbf..2c65a26 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -594,7 +594,7 @@ nextStep(5); setTimeout(() => { - window.location.href = '/login'; + window.location.href = '/download-tracker'; }, 1000); } else { // Handle server errors diff --git a/src/controllers/client/vaClientLogin.controller.js b/src/controllers/client/vaClientLogin.controller.js index 384c132..466fcea 100644 --- a/src/controllers/client/vaClientLogin.controller.js +++ b/src/controllers/client/vaClientLogin.controller.js @@ -71,7 +71,7 @@ async function va_login_client(req, res) { }, }); } catch (error) { - console.error("va_register error:", error); + console.error("va_client_login_register error:", error); return res.status(500).json({ success: false, message: "Internal Server Error" }); } } diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index 580552d..08a173b 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -1,6 +1,58 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import { VAModel, LoggedModel } from "../../../models/vaModel.js"; +import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js"; + +// function to close open work tracks and create logout +async function closePreviousSession(vaId, lastLog) { + try { + const currentTime = new Date(); + // Find all open work tracks for this VA and project (where to is not set or totalTime is 0) + const openWorkTracks = await WorkTrackModel.find({ + vaId, + logId: lastLog._id, + $or: [ + { to: { $exists: false } }, + { totalTime: 0 }, + { to: null } + ] + }); + + // console.log(`Found ${openWorkTracks.length} open work tracks to close`); + + // Close all open work tracks + for (const workTrack of openWorkTracks) { + const fromTime = new Date(workTrack.from); + const totalTimeMs = currentTime - fromTime; + + // Update the work track with end time and total time + workTrack.to = currentTime; + workTrack.totalTime = totalTimeMs; + await workTrack.save(); + + // console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`); + } + + // Create a logout log entry + const logoutLog = await LoggedModel.create({ + loggedType: "LOGOUT", + vaId, + vaEmail: lastLog.vaEmail, // Use email from previous log + projectId: lastLog.projectId, + time: currentTime // Use same timestamp for consistent logging + }); + + // console.log(`Created logout log: ${logoutLog._id}`); + + return { + closedWorkTracks: openWorkTracks.length, + logoutLogId: logoutLog._id + }; + + } catch (error) { + console.error("Error closing previous session:", error); + throw error; + } +} // Login VA async function va_login_callback(req, res) { @@ -32,36 +84,22 @@ async function va_login_callback(req, res) { return res.status(401).json({ success: false, message: "Invalid password" }); } + + // also on main login when user try to login into app then logout project and workModel track then allow login on same timestamp + // Check if VA has any active sessions (LOGIN without LOGOUT) const activeSessions = await LoggedModel.find({ - vaId: user._id, - loggedType: "LOGIN" + vaId: user._id }).sort({ time: -1 }); - + + let closeResult = []; + if (activeSessions.length > 0) { // Get the latest login to check if there's no corresponding logout - const latestLogin = activeSessions[0]; + const latestLog = activeSessions[0]; + closeResult = await closePreviousSession(user._id, latestLog); + } - // Check if there's a logout after this login - const correspondingLogout = await LoggedModel.findOne({ - vaId: user._id, - loggedType: "LOGOUT", - time: { $gt: latestLogin.time } - }); - - if (!correspondingLogout) { - // VA is still logged in to a project - const projectInfo = await LoggedModel.findById(latestLogin._id) - .populate('projectId', 'name') - .lean(); - - return res.status(400).json({ - success: false, - message: "You are still logged in to a project. Please logout first before logging in again." - }); - } - } - // Generate JWT accessToken with 24h expiry const accessToken = jwt.sign( { @@ -88,18 +126,40 @@ async function va_login_callback(req, res) { $set: { accessToken, refreshToken } }) + let responseData = []; + + if (closeResult) { + responseData = { + success: true, + message: "Login Successfull", + accessToken, + refreshToken, + user: { + id: user._id, + name: user.userDetails.firstName, + email: user.userDetails.email, + }, + previousSession: { + closedWorkTracks: closeResult.closedWorkTracks, + logoutLogId: closeResult.logoutLogId + } + } + } else { + responseData = { + success: true, + message: "Login Successfull", + accessToken, + refreshToken, + user: { + id: user._id, + name: user.userDetails.firstName, + email: user.userDetails.email, + } + } + } + // Return accessToken and user info - return res.status(200).json({ - success: true, - message: "Login Successfull", - accessToken, - refreshToken, - user: { - id: user._id, - name: user.userDetails.firstName, - email: user.userDetails.email, - }, - }); + return res.status(200).json(responseData); } catch (error) { console.error("va_register error:", error); return res.status(500).json({ success: false, message: "Internal Server Error" }); diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js index 54d0d73..a06daaa 100644 --- a/src/controllers/va/vaProjects/vaProjectLog.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js @@ -25,6 +25,59 @@ function formatHoursToReadable(hours) { } } +// function to close open work tracks and create logout +async function closePreviousSession(vaId, lastLog) { + try { + const currentTime = new Date(); + + // Find all open work tracks for this VA and project (where to is not set or totalTime is 0) + const openWorkTracks = await WorkTrackModel.find({ + vaId, + logId: lastLog._id, + $or: [ + { to: { $exists: false } }, + { totalTime: 0 }, + { to: null } + ] + }); + + // console.log(`Found ${openWorkTracks.length} open work tracks to close`); + + // Close all open work tracks + for (const workTrack of openWorkTracks) { + const fromTime = new Date(workTrack.from); + const totalTimeMs = currentTime - fromTime; + + // Update the work track with end time and total time + workTrack.to = currentTime; + workTrack.totalTime = totalTimeMs; + await workTrack.save(); + + // console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`); + } + + // Create a logout log entry + const logoutLog = await LoggedModel.create({ + loggedType: "LOGOUT", + vaId, + vaEmail: lastLog.vaEmail, // Use email from previous log + projectId: lastLog.projectId, + time: currentTime // Use same timestamp for consistent logging + }); + + // console.log(`Created logout log: ${logoutLog._id}`); + + return { + closedWorkTracks: openWorkTracks.length, + logoutLogId: logoutLog._id + }; + + } catch (error) { + console.error("Error closing previous session:", error); + throw error; + } +} + // Getting logged hours in a specific project of VA async function va_project_log_callback(req, res) { try { @@ -63,22 +116,83 @@ async function va_project_log_callback(req, res) { } // Check last log entry - const lastLog = await LoggedModel.findOne({ vaId, projectId }) + const lastLog = await LoggedModel.findOne({ vaId }) .sort({ time: -1 }) .lean(); // Prevent login if last action was login (not logout) if (lastLog && lastLog.loggedType === "LOGIN") { - return res.status(400).json({ - success: false, - message: "User is already logged in to this project" + + // check last workModel login time and make a logout request then create login + // console.log(`User already logged in. Closing previous session...`); + const closeResult = await closePreviousSession(vaId, lastLog); + + // console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`); + + // Now proceed to create new login + const createdLog = await LoggedModel.create({ + loggedType: "LOGIN", + vaId, + vaEmail, + projectId + }); + + return res.status(201).send({ + success: true, + message: `Login Successful (previous session closed)`, + logId: createdLog._id, + previousSession: { + closedWorkTracks: closeResult.closedWorkTracks, + logoutLogId: closeResult.logoutLogId + } }); } } const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId }) +// console.log(createdLog) + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + const sfData = { + "_id": createdLog._id, + "loggedType": type, + "vaId": vaId, + "vaEmail": vaEmail, + "projectId": projectId, + "time": createdLog.time, + } + + const response = await fetch(salesforceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify(sfData) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + const result = await response.json(); + // console.log("✅ Successfully synced VA data to Salesforce:", result); + + res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result }); + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } + + // return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); - return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); } catch (error) { return res.status(500).send("Internal Server Error"); } diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 4747eda..15dd444 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -163,6 +163,7 @@ const clientDetailsSchema = new Schema({ type: String, }, accountType: { + enum: ["CHECKING", "SAVINGS"], type: String, }, accountHolderName: { @@ -309,8 +310,8 @@ const vaWorkTrackSchema = new Schema({ ref: "Project", }, logId: { - type: String, - ref: "Logged", + type: Types.ObjectId, + ref: "TrackLog", }, title: { type: String, diff --git a/src/server.js b/src/server.js index 46c0973..7e80d5f 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,6 @@ import dotenv from "dotenv"; dotenv.config() -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; import express from 'express'; import { createServer } from 'http'; @@ -13,13 +12,29 @@ 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"; +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"); const PORT = process.env.PORT || 8000; @@ -39,10 +54,37 @@ app.use(bodyParser.text()); // API Route app.use('/api', apiRoutes); -app.get('/varegister/:sf_token?', (req, res) => { - // const { sf_token } = useParams(); +app.get("/download-tracker", (req, res) => { + res.render("download_tracker", { + version: "3.2.1" + }); +}); + +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; + + try { + const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN); + + if (decoded) { + res.render('va_register', { title: "VA Registration" }); + } else { + res.render('not_found', { title: "VA Registration" }); + } + } catch (error) { + res.render('not_found', { title: "VA Registration" }); + } - res.render('va_register', { title: "VA Registration" }); // if(verified) { // res.render('va_register', { title: "VA Registration" }); // } else { @@ -52,6 +94,9 @@ app.get('/varegister/:sf_token?', (req, res) => { app.get('/clientregister/:sf_token?', (req, res) => { + // portalId: RECORDid => 8990928342309 + // email + // const { sf_token } = useParams(); res.render('va_client_register', { title: "Client Registration" }); @@ -123,13 +168,30 @@ app.get('/client/virtual-assistants', async (req, res) => { // } // }); -app.get("/live/:projectId/:vaId", (req, res) => { - const { projectId } = req.params; - res.render("va_live_tracker", { title: "VA Live Tracker", projectId }) +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 jwt from "jsonwebtoken"; import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js'; import { ObjectId } from 'mongodb/lib/bson.js'; @@ -154,7 +216,7 @@ io.use(async (socket, next) => { return next(new Error("Invalid token payload")); } - /* ---------------------- 1⃣ Check if VA ---------------------- */ + /* ---------------------- 1️⃣ Check if VA ---------------------- */ let vaUser = null; let clientData = null; @@ -179,13 +241,13 @@ io.use(async (socket, next) => { console.log(a, b, c, "vaProjClientId vs clientData.clientId"); - vaSocketRespectToClient.set(vaProjClientId.toString(), socket.id) + vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}`, socket.id) console.log("✅ Authenticated VA:", vaUser.userDetails.email); return next(); } - /* ---------------------- 2⃣ Check if Client ---------------------- */ + /* ---------------------- 2️⃣ Check if Client ---------------------- */ // const clientUser = await ClientModel.findOne({ "userDetails.email": decoded.email }); // if (clientUser) { @@ -287,10 +349,10 @@ io.on("connection", (socket) => { socket.on("selectApp", ({ appTitle, clientId }) => { console.log(`VA selected app: ${appTitle}`); - const sssrrrr = vaSocketRespectToClient.get(clientId); - console.log(vaSocketRespectToClient.get(clientId), sssrrrr, "vaSocketRespectToClient[clientId]") + const sssrrrr = vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`); + console.log(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`), sssrrrr, "vaSocketRespectToClient[clientId]") // You can forward this info to the respective Electron client - socket.to(vaSocketRespectToClient.get(clientId)).emit("appToCapture", { appTitle }); + socket.to(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`)).emit("appToCapture", { appTitle }); }); socket.on("getVisibleApps", async ({ token, apps }) => { @@ -331,7 +393,6 @@ io.on("connection", (socket) => { - // io.on('connection', (socket) => { // socket.on("sendImage", (base64String) => { @@ -359,6 +420,12 @@ io.on("connection", (socket) => { // console.log(`Express started on http://localhost:${PORT}`); // }); +app.use((req, res) => { + res.status(404).render("not_found", { + title: "404 - Page Not Found" + }); +}); + server.listen(PORT, () => { mongoose .connect( diff --git a/src/services/salesforce.service.js b/src/services/salesforce.service.js index 3f310ad..f82ed1b 100644 --- a/src/services/salesforce.service.js +++ b/src/services/salesforce.service.js @@ -43,8 +43,8 @@ class SalesforceService { const { userDetails } = clientData; return { - _id: _id, userDetails: { + _id: _id, name: userDetails.name, email: userDetails.email.toLowerCase(), address: userDetails.address || null, diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js index 0feaa4c..b6c971b 100644 --- a/src/services/vaClient.service.js +++ b/src/services/vaClient.service.js @@ -17,11 +17,13 @@ class VaClientService { // Sanitize and prepare data const clientData = this.sanitizeClientData(data); - - // Send data to Salesforce first - await this.salesforceService.registerClientInSalesforce(data, clientData._id); - const response = await this.clientRepository.create(clientData); + + if(response) { + await this.salesforceService.registerClientInSalesforce(data, response._id); + } + // Send data to Salesforce first + return response; } catch (error) { logger.error("Client Service - registerClient error: " + error.message); diff --git a/views/client_login.ejs b/views/client_login.ejs index 5defecf..35a0ed6 100644 --- a/views/client_login.ejs +++ b/views/client_login.ejs @@ -212,7 +212,7 @@
@@ -267,7 +267,7 @@ headers: { "Content-Type": "application/json", }, - body: JSON.stringify({"userDetails": userDetails}), + body: JSON.stringify({ "userDetails": userDetails }), }); const data = await res.json(); @@ -276,10 +276,10 @@ throw new Error(data.message || "Login failed"); } - if (data.accessToken) { + if (data.success) { // 🟢 Save token localStorage.setItem("knoptpq", data.accessToken); - localStorage.setItem("refreshToken", data.refreshToken); + // localStorage.setItem("refreshToken", data.refreshToken); // 🟢 Redirect window.location.href = "/client/dashboard"; diff --git a/views/download_tracker.ejs b/views/download_tracker.ejs new file mode 100644 index 0000000..a10ef66 --- /dev/null +++ b/views/download_tracker.ejs @@ -0,0 +1,180 @@ + + + + + + + Download DocTracker + + + + + +
+

Download DocTracker

+ +

+ Let your clients see your real-time work logs, ensuring
+ you get paid accurately for every minute you've put in. +

+ +
+
+ windows + Windows +
+
+ macos + MacOS +
+
+ +
+ + +
+
+ + + + + + \ No newline at end of file diff --git a/views/not_found.ejs b/views/not_found.ejs new file mode 100644 index 0000000..e0a32cf --- /dev/null +++ b/views/not_found.ejs @@ -0,0 +1,129 @@ + + + + + + + 404 - Page Not Found + + + + + + +
+ 404 Icon +

404

+

Oops! The page you're looking for does not exist.

+ Go Home +
+ + + + + + + \ No newline at end of file diff --git a/views/va_live_tracker.ejs b/views/va_live_tracker.ejs index d05223c..d5a3a69 100644 --- a/views/va_live_tracker.ejs +++ b/views/va_live_tracker.ejs @@ -4,140 +4,279 @@ Live Screen Feed + -