Compare commits

...

2 Commits

Author SHA1 Message Date
=bhagwanSingh e83cbaafa8 a 2025-11-07 12:00:42 +05:30
=bhagwanSingh 50b3b88762 07-11 2025-11-07 11:43:43 +05:30
5 changed files with 422 additions and 216 deletions

View File

@ -2,6 +2,22 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../models/vaModel.js";
async function logVaTime(req, res) {
try {
const payload = req.body || {};
const { vaId, vaEmail, projectId, type } = payload;
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
await LoggedModel.insert({ loggedType: type, vaId, vaEmail, projectId })
return res.status(201).send({ success: true, message: `${type} Successfully` });
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
// Getting all active VA's and their details
async function va_callback(req, res) {
try {
@ -113,7 +129,7 @@ async function va_login_callback(req, res) {
return res.status(401).json({ success: false, message: "Invalid password" });
}
// Generate JWT accessToken with 24h expiry
// Generate JWT accessToken with 24h expiry
const accessToken = jwt.sign(
{
id: user._id,
@ -124,7 +140,7 @@ async function va_login_callback(req, res) {
{ expiresIn: "24h" }
);
// Generate JWT refreshToken with 24h expiry
// Generate JWT refreshToken with 24h expiry
const refreshToken = jwt.sign(
{
id: user._id,
@ -135,10 +151,14 @@ async function va_login_callback(req, res) {
{ expiresIn: "30d" }
);
await VAModel.updateOne({ "userDetails.email": userDetails.email }, {
$set: { accessToken, refreshToken }
})
// Return accessToken and user info
return res.status(200).json({
success: true,
message: "Login successful",
message: "Login Successfull",
accessToken,
refreshToken,
user: {
@ -152,6 +172,33 @@ async function va_login_callback(req, res) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
async function va_logout_callback(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const vaId = req.body.vaId || {};
const vaEmail = req.body.vaEmail || {};
if (!vaId || !vaEmail) return res.status(400).json({ success: false, message: "Invalid Credentials" });
// Find user by email
const user = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" });
}
await VAModel.updateOne({ "userDetails.email": userDetails.email }, {
$set: { accessToken: null, refreshToken: null }
})
// Return accessToken and user info
return res.status(200).json({
success: true,
message: "Log Out Successfull",
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Refresh Token VA
async function va_refresh_token_callback(req, res) {
@ -358,35 +405,110 @@ async function va_clients_projects_callback(req, res) {
}
}
// Getting VA log between specific dates with optional screenshot
async function va_log_callback(req, res) {
try {
const { id, from, to, screentShot } = req.params;
return res.status(200).json({
const { projectId, from, to } = req.params;
const { vaId } = req.body;
if (!projectId) return res.status(400).json({ success: false, message: "Missing Props" })
const project = await ProjectModel.findById(projectId);
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
// Default range
const fromDate = from && from !== "undefined" ? new Date(from) : new Date(project.startDate || "2000-01-01");
const toDate = to && to !== "undefined" ? new Date(to) : new Date();
// If from and to are the same day
if (from === to) {
fromDate.setHours(0, 0, 0, 0);
toDate.setHours(23, 59, 59, 999);
} else {
// Otherwise use them as a normal range
toDate.setHours(23, 59, 59, 999);
}
const query = {
vaId: vaId,
projectId,
time: {
$gte: fromDate,
$lte: toDate
}
};
const logs = await LoggedModel.find(query).sort({ time: 1 });
if (!logs.length) {
return res.json({ success: true, message: "No logs found", totalWork: "0h 0m" });
}
// Find first LOGIN and last LOGOUT
const firstLogin = logs.find((l) => l.loggedType === "LOGIN");
const lastLogout = [...logs].reverse().find((l) => l.loggedType === "LOGOUT");
if (!firstLogin || !lastLogout) {
return res.json({
success: true,
message: "Incomplete log data (missing LOGIN or LOGOUT)",
totalWork: "0h 0m",
firstLogin,
lastLogout,
});
}
// Calculate duration
const diffMs = new Date(lastLogout.time) - new Date(firstLogin.time);
const totalMinutes = Math.floor(diffMs / (1000 * 60));
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
res.json({
success: true,
message: "VA Log Callback",
id,
from,
to,
screentShot
vaId,
projectId,
firstLogin: firstLogin.time,
lastLogout: lastLogout.time,
logs: logs,
totalTimeLogged: `${hours}h ${minutes}m`,
hoursLogged: hours,
minutesLogged: minutes,
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: err.message });
}
}
};
// Getting logged hours in a specific project of VA
async function va_project_log_callback(req, res) {
try {
const { id, projectID } = req.params;
return res.status(200).json({
success: true,
message: "VA Project Log Callback",
id,
projectID
});
const payload = req.body || {};
const { vaId, vaEmail, projectId, type } = payload;
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
return res.status(201).send({ success: true, message: `${type} Successfully` });
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
return res.status(500).send("Internal Server Error");
}
}
async function va_time_logged_on_proj(req, res) {
try {
const payload = req.body || {};
const { vaId, vaEmail, projectId, type } = payload;
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
return res.status(201).send({ success: true, message: `${type} Successfully` });
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
@ -477,6 +599,7 @@ async function va_filter_search(req, res) {
export {
logVaTime,
va_callback,
va_username_callback,
va_clients_callback,
@ -487,5 +610,6 @@ export {
va_register,
va_projects_callback,
va_login_callback,
va_refresh_token_callback
va_logout_callback,
va_refresh_token_callback,
};

View File

@ -1,76 +1,36 @@
import AuthRepository from "../repository/auth.repository.js";
import AuthService from "../services/auth.service.js";
import TemplateRepository from "../repository/template.repository.js";
import TemplateService from "../services/template.service.js";
import httpReqRefToken from "../httpReq/refreshToken.httpReq.js";
import httpReqValidateAuth from "../httpReq/validateAuth.httpReq.js";
import { decryptCryptr, encryptCryptr } from "../helper/cryptrEncDec.js";
import logger from "../config/logger.js";
const authService = new AuthService(new AuthRepository());
const templateService = new TemplateService(new TemplateRepository());
import jwt from "jsonwebtoken";
import { VAModel } from "../models/vaModel.js";
async function authValidator(req, res, next) {
try {
logger.info("AuthValidator - middleware is running ...")
logger.info("Process ID", process.pid)
let templateId = null;
let orgId;
if ('template' in req.body && 'org_auth_id' in req.body.template && req.body.template['org_auth_id'] != null) {
const accessToken = req.headers.token;
if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" });
orgId = req.body.template.org_auth_id;
} else {
if ('templateId' in req.body) {
templateId = req.body.templateId;
} else if ('id' in req.body.template) {
templateId = req.body.template.id;
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) {
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail })
if (!vaUser) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (!("accessToken" in vaUser) || !vaUser.accessToken) {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
if (vaUser.accessToken === accessToken) {
req.body.vaId = vaUser._id
req.body.vaEmail = vaEmail
next();
} else {
return res.status(401).json({ success: false, message: "Not Authorized" });
}
const tempRes = await templateService.fetch({ _id: templateId });
const isPublish = tempRes.json_template !== null;
req.body.isPublish = isPublish;
orgId = tempRes.org_auth_id;
}
let authEntryExist = await authService.fetchByOrgId(orgId);
if (!authEntryExist) {
logger.error("AuthValidator - Entry not exist templateId:", templateId);
return res.status(403).send({"message": "Entry not exist"});
}
let { instance_url, refresh_token, access_token, is_sandbox } = authEntryExist;
access_token = decryptCryptr(access_token)
refresh_token = decryptCryptr(refresh_token)
let isValid = await httpReqValidateAuth(orgId, access_token, instance_url, refresh_token)
if (!isValid) {
logger.info("AuthValidator - Access_Token expired - refreshing ...");
const refTokenRes = await httpReqRefToken(refresh_token, is_sandbox);
access_token = refTokenRes.access_token;
await authService.update({ org_id: orgId }, { access_token: encryptCryptr(access_token) });
}
isValid = await httpReqValidateAuth(orgId, access_token, instance_url, refresh_token)
if (!isValid) {
logger.error("AuthValidator - Not Refreshing RefreshingToken Expired return 401. access_token");
return res.send(401).status({"message": "Token expired (acc | ref)"})
}
req.body.access_token = access_token;
req.body.instance_url = instance_url;
logger.info("AuthValidator - middleware Work Done (VALIDATED SUCCESSFULLY) ...")
next();
} catch (error) {
logger.error("AuthValidator - Middleware (CATCH Block)", error.message);
return res.send(401).status({"message": error.message})
return res.send(401).status({ "message": error.message })
}
}

View File

@ -62,6 +62,7 @@ const projectSchema = new Schema({
},
totalLoggedHours: {
type: Number,
default: 0
},
vaAssigned: [
{
@ -113,6 +114,14 @@ const loggedSchema = new Schema({
vaId: {
type: Types.ObjectId,
ref: "VA"
},
vaEmail: {
type: String,
required: true
},
projectId: {
type: Types.ObjectId,
ref: "Project"
}
})
@ -127,6 +136,12 @@ const vaSchema = new Schema(
ref: "Client",
},
],
projectIds: [
{
type: Types.ObjectId,
ref: "Project",
},
],
schedule: {
type: String,
},
@ -135,14 +150,47 @@ const vaSchema = new Schema(
enum: ["PARTTIME", "FULLTIME", "INACTIVE"],
default: "FULLTIME",
},
accessToken: {
type: String,
default: null
},
refreshToken: {
type: String,
default: null
}
},
{ timestamps: true }
);
// Need to add working hours and time zone tooooo in which VA is going to work, need to calculate total work hours of the day - Schedules
// Need to add this Schedule feature on SF there we can change the schedulte of VA - Add in form tooo
// Need to ask to client if we want to provide schedule changing functionality on Docker toooooo
// Add IN OUT
// const vaTimeSheetSchema = new Schema({
// vaId: {
// type: Types.ObjectId,
// ref: "VA",
// },
// projectId: {
// type: Types.ObjectId,
// ref: "Project",
// },
// logged: {
// enum: ["IN", "OUT"],
// default: "IN",
// }
// },
// { timestamps: true }
// )
/* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema);
const ProjectModel = model("Project", projectSchema);
const LoggedModel = model("Logged", loggedSchema);
const LoggedModel = model("TrackLog", loggedSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel };

View File

@ -1,12 +1,15 @@
import express from "express";
const vaRoutes = express.Router();
import { va_callback, va_username_callback, va_clients_callback, va_clients_projects_callback, va_log_callback, va_project_log_callback, va_filter_search, va_projects_callback, va_register, va_login_callback, va_refresh_token_callback } from "../../controllers/va.controller.js";
import { va_callback, va_username_callback, va_clients_callback, va_clients_projects_callback, va_log_callback, va_project_log_callback, va_filter_search, va_projects_callback, va_register, va_login_callback, va_refresh_token_callback, va_logout_callback } from "../../controllers/va.controller.js";
import { authValidator } from "../../middlewares/auth.middleware.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA
@ -19,9 +22,10 @@ vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callb
vaRoutes.get("/project/:projectId", va_projects_callback); // Projects Callback for getting project details
vaRoutes.get("/:id/log/:from/:to/:screentShot", va_log_callback); // VA Log Callback
// FROM || TO -
vaRoutes.get("/log/:projectId/:from?/:to?", authValidator, va_log_callback); // VA Log Callback
vaRoutes.get("/:id/log/project/:projectID", va_project_log_callback); // VA Logged Hours in specific Project
vaRoutes.post("/log/project", authValidator, va_project_log_callback); // VA Logged Hours in specific Project
// VA filter search: supports query params `name`, `email`, `from`, `to`
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31

View File

@ -4,132 +4,202 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VA Registration</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/public/css/va-register.css">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', sans-serif;
}
.card-registration {
border: none;
border-radius: 12px;
overflow: hidden;
}
.card-registration img {
height: 100%;
object-fit: cover;
}
.card-body {
padding: 3rem !important;
}
h3 {
font-weight: 600;
color: #343a40;
margin-bottom: 2rem;
}
.form-outline {
position: relative;
}
.form-control-lg {
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 1rem;
}
.form-label {
font-weight: 500;
color: #495057;
margin-bottom: 0.5rem;
}
.btn-lg {
border-radius: 8px;
padding: 0.7rem 2rem;
font-size: 1rem;
}
.btn-warning {
background-color: #ffc107;
border: none;
font-weight: 600;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-light {
border: 1px solid #ccc;
font-weight: 500;
}
h5 {
font-weight: 600;
color: #495057;
border-bottom: 2px solid #ffc107;
display: inline-block;
padding-bottom: 0.25rem;
margin-top: 2rem;
}
@media (max-width: 991px) {
.card-body {
padding: 2rem !important;
}
.card-registration img {
display: none;
}
}
</style>
</head>
<body>
<section class="h-100 py-5">
<div class="container">
<div class="row justify-content-center align-items-center">
<div class="col-lg-10 col-xl-9">
<div class="card card-registration shadow-lg">
<div class="row g-0">
<section class="h-100">
<div class="container py-1">
<div class="row d-flex justify-content-center align-items-center">
<div class="col">
<div class="card card-registration my-4 shadow">
<div class="row g-0">
<!-- Left Side Image -->
<div class="col-xl-6 d-none d-xl-block">
<img
src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-registration/img4.webp"
alt="Registration Image"
class="img-fluid"
style="border-top-left-radius: .25rem; border-bottom-left-radius: .25rem;"
/>
</div>
<!-- Right Side Form -->
<div class="col-xl-6">
<div class="card-body p-md-5 text-black">
<h3 class="mb-5 text-uppercase text-center">Virtual Assistant Registration</h3>
<form id="vaForm">
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<input type="text" id="firstName" name="userDetails[firstName]" class="form-control form-control-lg" required />
<label class="form-label" for="firstName">First Name *</label>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<input type="text" id="lastName" name="userDetails[lastName]" class="form-control form-control-lg" required/>
<label class="form-label" for="lastName">Last Name</label>
</div>
</div>
</div>
<div class="form-outline mb-4">
<input type="email" id="email" name="userDetails[email]" class="form-control form-control-lg" required />
<label class="form-label" for="email">Email *</label>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<input type="text" id="age" name="userDetails[age]" class="form-control form-control-lg not-required" placeholder=""/>
<label class="form-label" for="age">Age</label>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<input type="date" id="dob" name="userDetails[dob]" class="form-control form-control-lg" />
<label class="form-label" for="dob">Date of Birth</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="form-outline">
<input type="password" id="password" name="userDetails[password]" class="form-control form-control-lg" required />
<label class="form-label" for="password">Password *</label>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="form-outline">
<input type="password" id="cPassword" name="userDetails[cPassword]" class="form-control form-control-lg" required />
<label class="form-label" for="cPassword">Confirm Password *</label>
</div>
</div>
</div>
<div class="form-outline mb-4">
<input type="text" id="address" name="userDetails[address]" class="form-control form-control-lg not-required" placeholder=""/>
<label class="form-label" for="address">Address</label>
</div>
<h5 class="mt-4 mb-3">Bank Details</h5>
<div class="form-outline mb-4">
<input type="text" id="bankName" name="userDetails[bankName]" class="form-control form-control-lg" required/>
<label class="form-label" for="bankName">Bank Name</label>
</div>
<div class="form-outline mb-4">
<input type="text" id="accountHolderName" name="userDetails[accountHolderName]" class="form-control form-control-lg" required/>
<label class="form-label" for="accountHolderName">Account Holder Name</label>
</div>
<div class="form-outline mb-4">
<input type="text" id="accountNumber" name="userDetails[accountNumber]" class="form-control form-control-lg" required/>
<label class="form-label" for="accountNumber">Account Number</label>
</div>
<div class="form-outline mb-4">
<input type="text" id="routingNumber" name="userDetails[routingNumber]" class="form-control form-control-lg" required/>
<label class="form-label" for="routingNumber">Routing Number</label>
</div>
<div class="mb-4">
<label class="form-label" for="status">Status</label>
<select id="status" name="status" class="form-select form-select-lg">
<option value="FULLTIME">Full-Time</option>
<option value="PARTTIME">Part-Time</option>
</select>
</div>
<div class="d-flex justify-content-end pt-3">
<button type="reset" class="btn btn-light btn-lg">Reset all</button>
<button type="submit" class="btn btn-warning btn-lg ms-2">Submit form</button>
</div>
</form>
<!-- Left Image -->
<div class="col-xl-5 d-none d-xl-block">
<img
src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-registration/img4.webp"
alt="Registration"
class="img-fluid"
/>
</div>
<!-- Right Form -->
<div class="col-xl-7 bg-white">
<div class="card-body text-black">
<h3 class="text-center mb-4">Virtual Assistant Registration</h3>
<form id="vaForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="firstName">First Name *</label>
<input type="text" id="firstName" name="userDetails[firstName]" class="form-control form-control-lg" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="lastName">Last Name *</label>
<input type="text" id="lastName" name="userDetails[lastName]" class="form-control form-control-lg" required>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="email">Email *</label>
<input type="email" id="email" name="userDetails[email]" class="form-control form-control-lg" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="age">Age</label>
<input type="text" id="age" name="userDetails[age]" class="form-control form-control-lg">
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="dob">Date of Birth</label>
<input type="date" id="dob" name="userDetails[dob]" class="form-control form-control-lg">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="password">Password *</label>
<input type="password" id="password" name="userDetails[password]" class="form-control form-control-lg" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="cPassword">Confirm Password *</label>
<input type="password" id="cPassword" name="userDetails[cPassword]" class="form-control form-control-lg" required>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="address">Address</label>
<input type="text" id="address" name="userDetails[address]" class="form-control form-control-lg">
</div>
<h5>Bank Details</h5>
<div class="mb-3">
<label class="form-label" for="bankName">Bank Name</label>
<input type="text" id="bankName" name="userDetails[bankName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountHolderName">Account Holder Name</label>
<input type="text" id="accountHolderName" name="userDetails[accountHolderName]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="accountNumber">Account Number</label>
<input type="text" id="accountNumber" name="userDetails[accountNumber]" class="form-control form-control-lg" required>
</div>
<div class="mb-3">
<label class="form-label" for="routingNumber">Routing Number</label>
<input type="text" id="routingNumber" name="userDetails[routingNumber]" class="form-control form-control-lg" required>
</div>
<div class="mb-4">
<label class="form-label" for="status">Status</label>
<select id="status" name="status" class="form-select form-select-lg">
<option value="FULLTIME">Full-Time</option>
<option value="PARTTIME">Part-Time</option>
</select>
</div>
<div class="d-flex justify-content-end gap-2 pt-3">
<button type="reset" class="btn btn-light btn-lg">Reset</button>
<button type="submit" class="btn btn-warning btn-lg">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Custom JS -->
<script src="/public/js/va-register.js"></script>
</section>
<script src="/public/js/va-register.js"></script>
</body>
</html>