Added validation on login and project on
This commit is contained in:
parent
d50c18856f
commit
9429caacd6
|
|
@ -594,7 +594,7 @@
|
|||
|
||||
nextStep(5);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
window.location.href = '/download-tracker';
|
||||
}, 1000);
|
||||
} else {
|
||||
// Handle server errors
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@
|
|||
|
||||
<body>
|
||||
<div class="logo">
|
||||
<h2>DocTracker Client Login</h2>
|
||||
<img src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
|
||||
</div>
|
||||
|
||||
<div class="login-card">
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Download DocTracker</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* align-items: center; */
|
||||
height: 100vh;
|
||||
background: #ffffff;
|
||||
color: #111;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 630px;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
color: #1a168b;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.os-select {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.os-btn {
|
||||
padding: 12px 22px;
|
||||
border-radius: 30px;
|
||||
border: 2px solid #ddd;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.os-btn img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.active img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.os-btn.active {
|
||||
background: #0a0670;
|
||||
color: #fff;
|
||||
border-color: #0a0670;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.download-btn {
|
||||
margin: 0 auto;
|
||||
padding: 14px 25px;
|
||||
background: #d4d7f7;
|
||||
color: #0a0670;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.download-btn img {
|
||||
filter: brightness(0) invert(1);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
#downloadBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 15px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background: #c5c8f0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Download <span>DocTracker</span></h1>
|
||||
|
||||
<p class="desc">
|
||||
Let your clients see your real-time work logs, ensuring<br />
|
||||
you get paid accurately for every minute you've put in.
|
||||
</p>
|
||||
|
||||
<div class="os-select">
|
||||
<div class="os-btn active" data-os="windows">
|
||||
<img src="/public/images/windows.png" alt="windows">
|
||||
<span>Windows</span>
|
||||
</div>
|
||||
<div class="os-btn" data-os="mac">
|
||||
<img src="/public/images/mac-os-logo.png" alt="macos">
|
||||
<span>MacOS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="download-btn">
|
||||
<!-- <img src="/public/images/downloads.png" alt="macos" > -->
|
||||
<button id="downloadBtn">
|
||||
Download Windows version 3.2.1
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const osButtons = document.querySelectorAll(".os-btn");
|
||||
const downloadBtn = document.getElementById("downloadBtn");
|
||||
const version = "<%= version || '3.2.1' %>";
|
||||
|
||||
let selectedOS = "windows";
|
||||
|
||||
// -------- Auto OS Detection --------
|
||||
const platform = navigator.userAgent.toLowerCase();
|
||||
if (platform.includes("mac")) selectedOS = "mac";
|
||||
else selectedOS = "windows";
|
||||
|
||||
// Apply pre-selection
|
||||
osButtons.forEach(btn => {
|
||||
btn.classList.toggle("active", btn.dataset.os === selectedOS);
|
||||
});
|
||||
|
||||
// Update button text
|
||||
downloadBtn.innerHTML =
|
||||
`Download ${selectedOS === 'windows' ? 'Windows' : 'MacOS'} version ${version}`;
|
||||
|
||||
// -------- Manual Selection by User --------
|
||||
osButtons.forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
osButtons.forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
selectedOS = btn.dataset.os;
|
||||
|
||||
downloadBtn.innerHTML =
|
||||
`Download ${selectedOS === 'windows' ? 'Windows' : 'MacOS'} version ${version}`;
|
||||
});
|
||||
});
|
||||
|
||||
// -------- Download --------
|
||||
downloadBtn.addEventListener("click", () => {
|
||||
window.location.href = `/download-file?os=${selectedOS}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>404 - Page Not Found</title>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--text: #222222;
|
||||
--subtext: #555555;
|
||||
--button-bg: #6c63ff;
|
||||
--button-text: #ffffff;
|
||||
}
|
||||
|
||||
/* Dark mode automatic */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0f0f0f;
|
||||
--text: #ffffff;
|
||||
--subtext: #bbbbbb;
|
||||
--button-bg: #6c63ff;
|
||||
--button-text: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 7rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 10px;
|
||||
color: var(--subtext);
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 30px;
|
||||
padding: 14px 26px;
|
||||
background: var(--button-bg);
|
||||
color: var(--button-text);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.85;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 150px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
h1 {
|
||||
font-size: 4.5rem;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<img src="/public/images/logo-white-vertical.svg" alt="404 Icon">
|
||||
<h1>404</h1>
|
||||
<p>Oops! The page you're looking for does not exist.</p>
|
||||
<a href="/" class="btn">Go Home</a>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
function applyTheme() {
|
||||
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const logo = document.querySelector("img");
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add("dark");
|
||||
logo.src = "/public/images/logo-white-vertical.svg"; // Dark logo for light mode
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
logo.src = "/public/images/logo-colored-vertical.svg"; // White logo for dark mode
|
||||
}
|
||||
}
|
||||
|
||||
// Apply theme on load
|
||||
applyTheme();
|
||||
|
||||
// Listen for OS theme changes
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -4,140 +4,279 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Live Screen Feed</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
display: flex;
|
||||
background: #0f0f0f;
|
||||
color: white;
|
||||
font-family: "Inter", sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: #1b1b1b;
|
||||
border-right: 1px solid #333;
|
||||
background: #181818;
|
||||
border-right: 1px solid #2b2b2b;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
transition: transform 0.35s ease;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sidebar h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app {
|
||||
background: #222;
|
||||
background: #252525;
|
||||
border: 1px solid #333;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
word-break: break-word;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
.app:hover {
|
||||
background: #333;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.app.active {
|
||||
background: #6C63FF;
|
||||
background: #6c63ff;
|
||||
border-color: #6c63ff;
|
||||
color: white;
|
||||
box-shadow: 0 0 10px rgba(108, 99, 255, 0.5);
|
||||
}
|
||||
|
||||
/* MAIN CONTENT */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 12px;
|
||||
#selectedAppTitle {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
#liveImage {
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
border: 2px solid #555;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #444;
|
||||
object-fit: contain;
|
||||
background: black;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
.placeholder {
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
color: #bbb;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* MOBILE RESPONSIVE */
|
||||
.mobile-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
background: #6C63FF;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
z-index: 9999;
|
||||
/* VERY HIGH: always visible */
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.mobile-toggle span {
|
||||
display: block;
|
||||
width: 26px;
|
||||
height: 3px;
|
||||
background: #fff;
|
||||
margin: 5px 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Sidebar Hidden on Mobile */
|
||||
@media (max-width: 780px) {
|
||||
body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
/* Dark overlay when sidebar opens */
|
||||
#overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: none;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
#overlay.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
<!-- Mobile Toggle Button -->
|
||||
<div class="mobile-toggle" id="toggleBtn">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Overlay -->
|
||||
<div id="overlay"></div>
|
||||
|
||||
<div class="sidebar" id="sidebar">
|
||||
<h3>Visible Apps</h3>
|
||||
<div id="appsList"></div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h2 id="selectedAppTitle">Select an app to view</h2>
|
||||
<img id="liveImage" src="" alt="Waiting for image..." class="no-image" />
|
||||
<img id="liveImage" alt="Live Feed" />
|
||||
<div id="placeholder" class="placeholder">No app selected</div>
|
||||
</div>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const clientId = "691311fece83c986eb8a4e89";
|
||||
const vaId = "<%= vaId %>";
|
||||
const clientId = "<%= clientId %>";
|
||||
const token = localStorage.getItem("knoptpq");
|
||||
const socket = io({
|
||||
auth: {
|
||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY5MTQ2NTFkODM2NDZhOTAwN2EzNDI4MCIsImVtYWlsIjoiYmhhZ3dhbi5yQGhpY2dsb2JhbHNvbHV0aW9ucy5jb20iLCJuYW1lIjoiYmhhZ3dhbiIsImlhdCI6MTc2Mjk0ODk3NSwiZXhwIjoxNzYzMDM1Mzc1fQ.WvmTvfOf8DvbqKsbvfyzda1ClP8qfllKFeJINtU6mnE",
|
||||
userType: "client",
|
||||
projectId: "<%= projectId %>"
|
||||
},
|
||||
auth: {
|
||||
token: token,
|
||||
userType: "client",
|
||||
projectId: "<%= projectId %>"
|
||||
},
|
||||
});
|
||||
|
||||
socket.emit("register", { role: "client", id: clientId, projectId: "<%= projectId %>" });
|
||||
socket.emit("register", {
|
||||
role: "client",
|
||||
id: vaId,
|
||||
projectId: "<%= projectId %>",
|
||||
clientId: clientId
|
||||
});
|
||||
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
const toggleBtn = document.getElementById("toggleBtn");
|
||||
const overlay = document.getElementById("overlay");
|
||||
|
||||
const appsList = document.getElementById("appsList");
|
||||
const liveImage = document.getElementById("liveImage");
|
||||
const selectedAppTitle = document.getElementById("selectedAppTitle");
|
||||
const placeholder = document.getElementById("placeholder");
|
||||
|
||||
let currentApp = null;
|
||||
|
||||
// 🟢 Receive visible apps from client
|
||||
socket.on("receiveVisibleApps", (apps) => {
|
||||
appsList.innerHTML = "";
|
||||
if (!apps?.length) {
|
||||
appsList.innerHTML = "<p>No visible apps</p>";
|
||||
return;
|
||||
}
|
||||
/* MOBILE SIDEBAR TOGGLE */
|
||||
toggleBtn.onclick = () => {
|
||||
sidebar.classList.add("open");
|
||||
overlay.classList.add("show");
|
||||
};
|
||||
|
||||
apps.forEach(app => {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("app");
|
||||
div.textContent = app.name || "Unnamed App";
|
||||
div.onclick = () => {
|
||||
document.querySelectorAll(".app").forEach(el => el.classList.remove("active"));
|
||||
div.classList.add("active");
|
||||
currentApp = app.name;
|
||||
selectedAppTitle.textContent = app.name;
|
||||
overlay.onclick = () => {
|
||||
sidebar.classList.remove("open");
|
||||
overlay.classList.remove("show");
|
||||
};
|
||||
|
||||
// 🟠 Tell server which app VA wants to monitor
|
||||
socket.emit("selectApp", { appTitle: app.name, clientId });
|
||||
};
|
||||
appsList.appendChild(div);
|
||||
});
|
||||
});
|
||||
/* Receive visible apps */
|
||||
socket.on("receiveVisibleApps", (apps) => {
|
||||
appsList.innerHTML = "";
|
||||
|
||||
// 🟣 Receive images only for the selected app
|
||||
socket.on("receiveImage", (base64, title) => {
|
||||
if (currentApp && title === currentApp) {
|
||||
liveImage.src = base64;
|
||||
liveImage.classList.remove("no-image");
|
||||
}
|
||||
});
|
||||
if (!apps?.length) {
|
||||
appsList.innerHTML = "<p style='color:#777;'>No visible apps</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
apps.forEach(app => {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("app");
|
||||
div.textContent = app.name || "Unnamed App";
|
||||
|
||||
div.onclick = () => {
|
||||
document.querySelectorAll(".app").forEach(el => el.classList.remove("active"));
|
||||
div.classList.add("active");
|
||||
|
||||
currentApp = app.name;
|
||||
selectedAppTitle.textContent = app.name;
|
||||
|
||||
placeholder.style.display = "none";
|
||||
liveImage.style.display = "none";
|
||||
|
||||
socket.emit("selectApp", { appTitle: app.name, clientId, vaId });
|
||||
|
||||
// Close sidebar on mobile
|
||||
sidebar.classList.remove("open");
|
||||
overlay.classList.remove("show");
|
||||
};
|
||||
|
||||
appsList.appendChild(div);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("authError", () => {
|
||||
socket.disconnect();
|
||||
localStorage.removeItem("knoptpq");
|
||||
window.location.href = "/client/login";
|
||||
});
|
||||
|
||||
/* Receive images */
|
||||
socket.on("receiveImage", (base64, title) => {
|
||||
if (currentApp && title === currentApp) {
|
||||
liveImage.src = base64;
|
||||
liveImage.style.display = "block";
|
||||
placeholder.style.display = "none";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue