This commit is contained in:
Aakash Mittal 2025-11-04 19:03:33 +05:30
commit 48276d7161
29 changed files with 11229 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
logs
.env

8
nodemon.json Normal file
View File

@ -0,0 +1,8 @@
{
"watch": [
"src",
"views"
],
"ext": "js,ejs",
"exec": "node src/server.js"
}

9240
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "formbuilder-express",
"version": "1.0.0",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "nodemon --inspect src/server.js",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npx webpack --mode production",
"dev": "npx webpack --mode development"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"app-root-path": "^3.1.0",
"axios": "^1.7.2",
"bcrypt": "^6.0.0",
"body-parser": "^1.20.2",
"clean-css": "^5.3.3",
"compression": "^1.8.1",
"cors": "^2.8.5",
"cryptr": "^6.3.0",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-minify": "^1.0.0",
"helmet": "^8.1.0",
"html-minifier": "^4.0.0",
"html-minifier-terser": "^7.2.0",
"html-pdf-node": "^1.0.8",
"jsforce": "^3.6.3",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.12.0",
"mongoose": "^8.9.3",
"node-cache": "^5.1.2",
"nodemon": "^3.1.0",
"prom-client": "^15.1.3",
"puppeteer": "^24.10.1",
"request": "^2.88.2",
"require": "^2.4.20",
"uglify-js": "^3.19.3",
"uuid": "^11.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@babel/cli": "^7.26.4",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"babel-loader": "^9.2.1",
"copy-webpack-plugin": "^12.0.2",
"terser-webpack-plugin": "^5.3.11",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-node-externals": "^3.0.0"
}
}

View File

@ -0,0 +1,70 @@
body {
background-color: #212529;
}
.card-registration {
border-radius: 15px;
}
.card-registration .form-control {
border-radius: 0.5rem;
}
.form-label {
font-weight: 500;
}
.btn-warning {
background-color: #ffb300;
border: none;
color: #000;
font-weight: 600;
}
.btn-warning:hover {
background-color: #e6a000;
}
.form-outline .form-control+.form-label {
position: absolute;
top: 6px;
max-width: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
left: .75rem;
padding-top: .37rem;
pointer-events: none;
transform-origin: 0 0;
transition: all .2s ease-out;
color: var(--mdb-form-control-label-color);
margin-bottom: 0;
background-color: #ffffff;
}
.form-outline .form-control {
outline: none;
box-shadow: none;
}
.form-outline {
position: relative;
width: 100%;
}
.form-outline .form-control.not-required:placeholder-shown + .form-label,
.form-outline .form-control.not-required:empty + .form-label {
top: 6px;
}
.form-outline .form-control:focus + .form-label,
.form-outline .form-control:valid + .form-label,
.form-outline .form-control.not-required:not(:placeholder-shown) + .form-label,
.form-outline .form-control.not-required:focus + .form-label {
top: -17px;
}
.card-registration .select-input.form-control[readonly]:not([disabled]) {
font-size: 1rem;
line-height: 2.15;
padding-left: .75em;
padding-right: .75em;
}

46
public/js/va-register.js Normal file
View File

@ -0,0 +1,46 @@
// Example: Basic client-side validation or UX improvements
document.addEventListener("DOMContentLoaded", function () {
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const password = document.getElementById("password").value;
const cPassword = document.getElementById("cPassword").value;
if (password !== cPassword) {
alert("Passwords do not match!");
return;
}
handleSubmit();
});
});
function handleSubmit() {
// Example: collect form data manually
const form = document.querySelector("form");
const formData = new FormData(form);
const payload = { userDetails: {} };
formData.forEach((value, key) => {
// handle nested names like userDetails[firstName]
if (key.startsWith("userDetails[")) {
const innerKey = key.match(/userDetails\[(.*?)\]/)[1];
payload.userDetails[innerKey] = value;
} else {
payload[key] = value;
}
});
// You could then send it via fetch:
fetch("/api/v1/va/register", {
method: "POST",
body: JSON.stringify(payload),
})
.then((res) => res.json())
.then((data) => {
console.log("Server Response:", data);
alert("Form submitted successfully!");
})
.catch((err) => console.error("Error:", err));
}

105
src/config/jsForceCon.js Normal file
View File

@ -0,0 +1,105 @@
import jsforce from "jsforce";
import AuthService from "../services/auth.service.js";
import AuthRepository from "../repository/auth.repository.js";
import { decryptCryptr, encryptCryptr } from "../helper/cryptrEncDec.js";
import { getTempFormId, getTempOrgid } from "../controllers/template.controllers.js";
import { get_instance_url } from "../controllers/auth.controllers.js";
import logger from "./logger.js";
import httpReqValidateAuth from "../httpReq/validateAuth.httpReq.js";
import httpReqRefToken from "../httpReq/refreshToken.httpReq.js";
const authService = new AuthService(new AuthRepository());
const jsForceSFCon = async (templateId) => {
let tempFormId = null;
let instanceUrl = ''
let refreshToken = ''
let accessToken = ''
let isSandBox = ''
let tempOrgId = null
try {
tempOrgId = await getTempOrgid(templateId);
tempFormId = await getTempFormId(templateId);
if (tempOrgId) {
const authEntryExist = await authService.fetchByOrgId(tempOrgId);
if (!authEntryExist) {
throw "form not exist";
}
instanceUrl = authEntryExist.instance_url;
refreshToken = decryptCryptr(authEntryExist.refresh_token);
accessToken = decryptCryptr(authEntryExist.access_token);
isSandBox = authEntryExist.is_sandbox;
const iurl = await get_instance_url(tempOrgId)
if (iurl) {
instanceUrl = iurl
}
}
} catch (error) {
logger.error(`jsForceCon - Error in jsForceConnection: ${error.message}`);
return;
}
try {
// 1️⃣ Proactively validate access token
const isValid = await httpReqValidateAuth(tempOrgId, accessToken, instanceUrl, refreshToken);
// 2️⃣ Refresh token if invalid
if (!isValid) {
logger.info("Access token invalid/expired, refreshing...");
const result = await httpReqRefToken(refreshToken, isSandBox);
accessToken = result.access_token;
instanceUrl = result.instance_url || instanceUrl;
// Save new access token in DB
await authService.update(
{ org_id: tempOrgId },
{ access_token: encryptCryptr(accessToken) }
);
logger.info("New access token saved in DB after refresh");
}
} catch (err) {
logger.error(`jsForceSFCon - Error validating or refreshing token: ${err.message}`);
return;
}
// Create a Jsforce connection using the provided access token and instance URL
const oauth2 = new jsforce.OAuth2({
loginUrl: isSandBox ? 'https://test.salesforce.com' : 'https://login.salesforce.com',
clientId: isSandBox ? process.env.CLIENT_ID_SAND : process.env.CLIENT_ID_PROD,
clientSecret: isSandBox ? process.env.CLIENT_SECRET_SAND : process.env.CLIENT_SECRET_PROD,
// redirectUri: process.env.REDIRECT_URI, // optional if not used in refresh
});
const conn = new jsforce.Connection({
oauth2,
instanceUrl,
accessToken,
refreshToken,
});
// Handle auto-refresh and persist new token
conn.on("refresh", async (newAccessToken) => {
logger.info("Salesforce token auto-refreshed");
try {
await authService.update(
{ org_id: tempOrgId },
{ access_token: encryptCryptr(newAccessToken) }
);
logger.info("New access token saved in DB");
} catch (err) {
logger.error("Failed to save refreshed token", err.message);
}
});
return conn;
}
export default jsForceSFCon;

55
src/config/logger.js Normal file
View File

@ -0,0 +1,55 @@
// logger.js
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import appRoot from 'app-root-path'; // or use `import path from 'path'` if preferred
const { combine, timestamp, json, printf } = winston.format;
const logFormat = printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} ${level}: ${message}`;
if (Object.keys(metadata).length) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
});
const logger = winston.createLogger({
level: 'info',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat
),
transports: [
new winston.transports.Console(),
new DailyRotateFile({
filename: `${appRoot.path}/logs/application-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
json()
)
})
],
exceptionHandlers: [
new winston.transports.Console(),
new DailyRotateFile({
filename: `${appRoot.path}/logs/exceptions-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
json()
)
})
],
exitOnError: false,
});
// ✅ Export using ES module syntax
export default logger;

View File

@ -0,0 +1,183 @@
import AuthRepository from "../repository/auth.repository.js";
import AuthService from "../services/auth.service.js";
import httpReqGetAuth from "../httpReq/getAuth.httpReq.js";
import httpReqValidateAuth from "../httpReq/validateAuth.httpReq.js";
import httpReqRefToken from "../httpReq/refreshToken.httpReq.js";
import { decryptCryptr, encryptCryptr } from "../helper/cryptrEncDec.js";
import logger from "../config/logger.js";
const authService = new AuthService(new AuthRepository());
async function auth_callback(req, res) {
logger.info("AuthController - Get Hit ...")
try {
var body = req.body; // orgId, isSandbox, code
let data = {
client_id: process.env.CLIENT_ID_PROD,
client_secret: process.env.CLIENT_SECRET_PROD,
redirect_uri: `${process.env.BASE_URI}api/v1/auth`,
code: body.code,
grant_type: "authorization_code",
};
let uri;
if (body.isSandbox) {
data.client_id = process.env.CLIENT_ID_SAND
data.client_secret = process.env.CLIENT_SECRET_SAND
uri = "https://test.salesforce.com/services/oauth2/token";
} else {
uri = "https://login.salesforce.com/services/oauth2/token";
}
const url = uri;
const orgId = body.orgId;
const isSandBox = body.isSandbox;
logger.info("AuthController - Getting OrgId", orgId);
let salesforce_data = await httpReqGetAuth(url, data, orgId, isSandBox);
const authEntryExist = await authService.fetchByOrgId(orgId);
salesforce_data = { ...salesforce_data, access_token: encryptCryptr(salesforce_data.access_token), refresh_token: encryptCryptr(salesforce_data.refresh_token) }
if (!authEntryExist) {
await authService.create({ ...salesforce_data, is_connected: true });
}
else {
await authService.update({ org_id: orgId }, { ...salesforce_data, is_connected: true });
}
logger.info("AuthController - Connected Successfully", orgId);
res.status(201).send("Connected Successfully");
} catch (error) {
logger.error(`Auth ControllerA ${error.message}`);
res.status(400).send("Connection Error");
}
}
async function auth_validate(req, res) {
try {
const orgId = req.params.orgId;
let authEntryExist = await authService.fetchByOrgId(orgId);
if (!authEntryExist) {
return res.sendStatus(400);
}
let { instance_url, refresh_token, access_token, is_sandbox } = authEntryExist;
access_token = decryptCryptr(access_token)
refresh_token = decryptCryptr(refresh_token)
const isValid = await httpReqValidateAuth(
orgId,
access_token,
instance_url,
refresh_token
);
if (!isValid) {
const { access_token } = await httpReqRefToken(
refresh_token, is_sandbox
);
await authService.update(
{ _id: authEntryExist._id },
{ access_token: encryptCryptr(access_token) }
);
}
return res.status(201);
} catch (error) {
logger.error(`Auth ControllerB ${error.message}`);
}
}
async function auth_connected(req, res) {
try {
logger.info("AuthConnected - Getting Hit ...");
const orgId = req.params.orgId;
const authEntryExist = await authService.fetchByOrgId(orgId);
logger.info("AuthConnected - OrgId", orgId, "Check", authEntryExist);
if (!authEntryExist) {
logger.error("AuthConnected - Entry Not Exist");
return res.status(400).send({ "message": "Entry Not Exist" });
}
let { instance_url, refresh_token, access_token, is_sandbox } = authEntryExist;
if (!access_token || !refresh_token) {
logger.error("Access or refresh token is missing");
return res.status(403).send({ message: "Missing token data" });
}
access_token = decryptCryptr(access_token)
refresh_token = decryptCryptr(refresh_token)
let isValid = await httpReqValidateAuth(orgId, access_token, instance_url, refresh_token)
if (!isValid) {
const { access_token } = await httpReqRefToken(refresh_token, is_sandbox);
await authService.update(
{ _id: authEntryExist._id },
{ access_token: encryptCryptr(access_token) }
);
isValid = await httpReqValidateAuth(orgId, access_token, instance_url, refresh_token)
}
logger.info("Auth_Connected Called Line 128", isValid)
if (isValid) {
return res.status(200).send({ message: "Connected" });
} else {
return res.status(403).send({ message: "Not Connected" });
}
} catch (error) {
logger.error(`Auth ControllerC ${error.message}`);
return res.status(403).send({ message: "Not Connected" });
}
}
async function auth_disconnect(req, res) {
try {
const orgId = req.params.orgId;
const authEntryExist = await authService.fetchByOrgId(orgId);
if (authEntryExist) {
const id = authEntryExist._id
await authService.remove(id);
return res.status(201).send({ message: "Disconnected" });
} else {
return res.status(401).send({ message: "Document not found" });
}
} catch (error) {
logger.error(`Auth ControllerD ${error.message}`);
}
}
async function get_instance_url(orgId) {
try {
const orgData = await authService.fetchByOrgId(orgId);
const instanceUrl = orgData.instance_url;
if (!instanceUrl || !orgData) throw new Error('Error Getting Instance Url');
return instanceUrl;
} catch (error) {
logger.error(`Auth ControllerE ${error.message}`);
}
}
export {
auth_callback,
auth_validate,
auth_connected,
auth_disconnect,
get_instance_url
};

View File

@ -0,0 +1,424 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../models/vaModel.js";
// Getting all active VA's and their details
async function va_callback(req, res) {
try {
// Find the latest log entry for each VA and return those whose last entry is LOGIN
const agg = await LoggedModel.aggregate([
{ $sort: { time: -1 } },
{ $group: { _id: "$vaId", lastLog: { $first: "$$ROOT" } } },
{ $match: { "lastLog.loggedType": "LOGIN" } }
]);
const vaIds = agg.map((a) => a._id).filter(Boolean);
if (vaIds.length === 0) {
return res.status(200).json({ success: true, count: 0, data: [] });
}
const vas = await VAModel.find({ _id: { $in: vaIds } }).lean().exec();
// map lastLog by VA id for easy attachment
const lastLogByVa = agg.reduce((acc, a) => {
acc[String(a._id)] = a.lastLog;
return acc;
}, {});
const results = vas.map((v) => ({ ...v, lastLog: lastLogByVa[String(v._id)] || null }));
return res.status(200).json({ success: true, count: results.length, data: results });
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
// Register a new VA
async function va_register(req, res) {
try {
const payload = req.body ? JSON.parse(req.body) : {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.password) {
return res.status(400).json({ success: false, message: "Missing required fields" });
}
if (userDetails.password !== userDetails.cPassword) {
return res.status(400).json({ success: false, message: "password not matching" });
}
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
// sanitize/construct allowed fields
const toCreate = {
userDetails: {
firstName: userDetails.firstName,
lastName: userDetails.lastName || "",
email: userDetails.email,
dob: userDetails.dob || null,
age: userDetails.age || null,
address: userDetails.address || null,
password: hashedPassword,
bankName: userDetails.bankName || null,
accountHolderName: userDetails.accountHolderName || null,
accountNumber: userDetails.accountNumber || null,
routingNumber: userDetails.routingNumber || null,
},
tracking: payload.tracking || undefined,
clientIds: payload.clientIds || [],
schedule: payload.schedule || undefined,
status: payload.status || "FULLTIME",
};
const created = await VAModel.create(toCreate);
return res.status(201).json({ success: true, data: created });
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Login VA
async function va_login_callback(req, res) {
try {
const payload = req.body ? JSON.parse(req.body) : {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.password) {
return res.status(400).json({ success: false, message: "Please provide email and password" });
}
// Find user by email
const user = await VAModel.findOne({ "userDetails.email": userDetails.email });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" });
}
// const saltRounds = 10;
// const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
// Compare password with stored hash
const isMatch = await bcrypt.compare(
userDetails.password,
user.userDetails.password
);
if (!isMatch) {
return res.status(401).json({ success: false, message: "Invalid password" });
}
// Generate JWT token with 24h expiry
const token = jwt.sign(
{
id: user._id,
email: user.userDetails.email,
name: user.userDetails.firstName,
},
process.env.JWT_SECRET, // e.g. "mySuperSecretKey"
{ expiresIn: "24h" }
);
// Return token and user info
return res.status(200).json({
success: true,
message: "Login successful",
token,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
},
});
return res.status(201).json({ success: true, data: 'created' });
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Getting VA details by username
async function va_username_callback(req, res) {
try {
const { userName } = req.params;
if (!userName) {
return res.status(400).json({ success: false, message: "Missing userName param" });
}
// Find VA by common identifier fields (id, email, username, first/last name)
const va = await VAModel.findOne({
$or: [
{ _id: userName },
{ "userDetails.email": userName },
{ "userDetails.userName": userName },
{ "userDetails.firstName": userName },
{ "userDetails.lastName": userName },
{ "userDetails.fullName": userName }
]
}).lean().exec();
if (!va) {
return res.status(404).json({ success: false, message: "VA not found" });
}
// Latest log entry to determine active state
const lastLog = await LoggedModel.findOne({ vaId: va._id }).sort({ time: -1 }).lean().exec();
const active = lastLog ? String(lastLog.loggedType).toUpperCase() === "LOGIN" : false;
// Fetch all logs for this VA (ordered ascending) to compute logged hours
const logs = await LoggedModel.find({ vaId: va._id }).sort({ time: 1 }).lean().exec();
let totalSeconds = 0;
if (logs.length) {
// If logs have an explicit duration field, sum it
if (logs[0].duration !== undefined) {
totalSeconds = logs.reduce((sum, l) => sum + (Number(l.duration) || 0), 0);
} else {
// Otherwise compute from LOGIN/LOGOUT pairs
let lastLogin = null;
for (const l of logs) {
const t = l.time ? new Date(l.time) : null;
const type = String(l.loggedType || "").toUpperCase();
if (type === "LOGIN" && t) {
lastLogin = t;
} else if (type === "LOGOUT" && t && lastLogin) {
const diff = (t.getTime() - lastLogin.getTime()) / 1000;
if (diff > 0) totalSeconds += diff;
lastLogin = null;
}
}
// If currently logged in, add time since last login
if (lastLogin) {
totalSeconds += (Date.now() - lastLogin.getTime()) / 1000;
}
}
}
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
const totalHours = `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
// Project / client info (fall back to empty array)
const projects = va.clientIds || va.projects || [];
const name = `${va.userDetails?.firstName || ""}`.trim() + (va.userDetails?.lastName ? ` ${va.userDetails.lastName}` : "");
const email = va.userDetails?.email || null;
return res.status(200).json({
success: true,
data: {
id: va._id,
name: name || null,
email,
active,
totalHours,
lastLog: lastLog || null,
projects
}
});
} catch (error) {
console.error("va_username_callback error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Getting all clients or specific client by ID (with projects)
async function va_clients_callback(req, res) {
try {
const { clientID } = req.params;
if (clientID) {
// Find client by ID
const client = await ClientModel.findById(clientID).lean();
if (!client) {
return res.status(404).json({ success: false, message: "Client not found" });
}
// Find projects for this client
let projects = [];
if (client.projectIds && client.projectIds.length) {
projects = await ProjectModel.find({ _id: { $in: client.projectIds } }).lean();
}
return res.status(200).json({
success: true,
data: {
client,
projects
}
});
} else {
// Pagination: ?page=N (default 1), 20 per page
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = 20;
const skip = (page - 1) * limit;
const [clients, total] = await Promise.all([
ClientModel.find().skip(skip).limit(limit).lean(),
ClientModel.countDocuments()
]);
return res.status(200).json({
success: true,
page,
pageSize: clients.length,
total,
data: clients
});
}
} catch (error) {
console.error("va_clients_callback error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Getting projects for a specific client
async function va_clients_projects_callback(req, res) {
try {
const { clientId, projectId } = req.params;
return res.status(200).json({
success: true,
message: "VA Clients Projects Callback",
clientId,
projectId
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// 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({
success: true,
message: "VA Log Callback",
id,
from,
to,
screentShot
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// 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
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// Getting project of details (Clients, VAs, Logs)
async function va_projects_callback(req, res) {
try {
const { projectId } = req.params;
return res.status(200).json({
success: true,
message: "VA Project Callback",
projectId
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
// New: VA filter search
// Query params supported: name, email, from, to
// - name: partial match against firstName or lastName (case-insensitive)
// - email: partial or full match against userDetails.email (case-insensitive)
// - from, to: ISO date strings (e.g. 2025-01-01) to filter logs by time
async function va_filter_search(req, res) {
try {
const { name, email, from, to } = req.query;
// Build VA query
const query = {};
const and = [];
if (name) {
const nameRegex = new RegExp(name, "i");
and.push({ $or: [{ "userDetails.firstName": nameRegex }, { "userDetails.lastName": nameRegex }] });
}
if (email) {
const emailRegex = new RegExp(email, "i");
and.push({ "userDetails.email": emailRegex });
}
if (and.length) query.$and = and;
// Find matching VAs
const vas = await VAModel.find(query).lean().exec();
// If no date range provided, return matched VAs only
if (!from && !to) {
return res.status(200).json({ success: true, count: vas.length, data: vas });
}
// Parse date range
const dateFilter = {};
if (from) {
const fromDate = new Date(from);
if (!isNaN(fromDate)) dateFilter.$gte = fromDate;
}
if (to) {
const toDate = new Date(to);
if (!isNaN(toDate)) dateFilter.$lte = toDate;
}
// If dateFilter is empty, just return vas
if (Object.keys(dateFilter).length === 0) {
return res.status(200).json({ success: true, count: vas.length, data: vas });
}
// Fetch logs for matching VAs in a single query
const vaIds = vas.map((v) => v._id).filter(Boolean);
const logs = await LoggedModel.find({ vaId: { $in: vaIds }, time: dateFilter }).lean().exec();
// Group logs by VA id
const logsByVa = logs.reduce((acc, log) => {
const key = String(log.vaId);
if (!acc[key]) acc[key] = [];
acc[key].push(log);
return acc;
}, {});
// Attach logs to each VA (as `logs` property)
const results = vas.map((v) => ({ ...v, logs: logsByVa[String(v._id)] || [] }));
return res.status(200).json({ success: true, count: results.length, data: results });
} catch (error) {
console.error("va_filter_search error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export {
va_callback,
va_username_callback,
va_clients_callback,
va_clients_projects_callback,
va_log_callback,
va_project_log_callback,
va_filter_search,
va_register,
va_projects_callback,
va_login_callback
};

View File

@ -0,0 +1,17 @@
import Cryptr from "cryptr"
const cryptr = new Cryptr('fme-emf-06-2024-06-made-emf-easy-forms-forfmems-madfmee-efmeasy', { encoding: 'base64', pbkdf2Iterations: 10000, saltLength: 10 });
const encryptCryptr = (str) => {
const encryptedString = cryptr.encrypt(str);
return encryptedString;
}
const decryptCryptr = (str) => {
const decryptedString = cryptr.decrypt(str);
return decryptedString;
}
export {
encryptCryptr,
decryptCryptr
}

View File

@ -0,0 +1,40 @@
import jsForceSFCon from '../config/jsForceCon.js';
import logger from '../config/logger.js';
// const authService = new AuthService(new AuthRepository());
async function getDataFromSF(templateId, recordId, objectNames, showParentPreFill, preFillParentObj) {
const conn = await jsForceSFCon(templateId);
// Fetch record by ID
const records = [];
for (const objectName of objectNames) {
try {
const record = await conn.sobject(objectName).retrieve(recordId);
console.log(`Fetching record for object: ${objectName}`);
records.push(record);
} catch (err) {
logger.error(`getDataFromSF - Error Getting Records: ${err.message}`);
}
}
console.log(`Fetching related records for object: ${preFillParentObj.object}`);
if(showParentPreFill && preFillParentObj.object && preFillParentObj.field) {
console.log(`Fetching related records for object: ${preFillParentObj.object} with field: ${preFillParentObj.field}`);
let recordId = null
for(const record of records) {
if(preFillParentObj.field in record) {
recordId = record[preFillParentObj.field];
}
}
console.log(`Related Record ID: ${recordId}`);
const record = await conn.sobject(preFillParentObj.object).retrieve(recordId);
records.push(record);
}
return records;
}
export default getDataFromSF;

View File

@ -0,0 +1,54 @@
import request from "request";
import logger from "../config/logger.js";
async function httpReqGetAuth(url, data, orgId, isSandBox) {
const salesforce_data = {
org_id: "",
access_token: "",
refresh_token: "",
instance_url: "",
is_sandbox: isSandBox,
};
// Wrapping request.post in a Promise
const postRequest = () => {
return new Promise((resolve, reject) => {
request.post(
{
url,
form: { ...data },
},
(error, response) => {
if (error) {
return reject(error);
}
try {
const resBody = JSON.parse(response.body);
if (!resBody.id.includes(orgId)) {
return reject(new Error("Org_id Mismatch"));
}
salesforce_data.org_id = orgId;
salesforce_data.instance_url = resBody.instance_url;
salesforce_data.access_token = resBody.access_token;
salesforce_data.refresh_token = resBody.refresh_token;
resolve(salesforce_data);
} catch (parseError) {
reject(parseError);
}
}
);
});
};
// Await the postRequest promise
try {
const result = await postRequest();
return result;
} catch (err) {
logger.error(`getAuth.httpReq - Error: ${JSON.stringify(err)}`);
}
}
export default httpReqGetAuth;

View File

@ -0,0 +1,49 @@
import request from "request";
import logger from "../config/logger.js";
async function httpReqRefToken(refresh_token, is_sandbox) {
const postRequest = () => {
let url = is_sandbox
? 'https://test.salesforce.com/services/oauth2/token'
: 'https://login.salesforce.com/services/oauth2/token';
// logger.log("httpReqRefToken - getting called url:", url, "is_sandbox:", is_sandbox, refresh_token);
return new Promise((resolve, reject) => {
request.post(
{
url: url,
form: {
client_id: is_sandbox ? process.env.CLIENT_ID_SAND : process.env.CLIENT_ID_PROD,
client_secret: is_sandbox ? process.env.CLIENT_SECRET_SAND : process.env.CLIENT_SECRET_PROD,
redirect_uri: process.env.REDIRECT_URI,
grant_type: "refresh_token",
refresh_token
}
},
(error, res, body) => {
if (error) return reject(error);
try {
const parsed = JSON.parse(body);
return resolve({ statusCode: res.statusCode, ...parsed });
} catch (parseErr) {
return reject(new Error("Failed to parse response from Salesforce"));
}
}
);
});
};
try {
const result = await postRequest();
if (result.statusCode === 400 || result.statusCode === 401) {
throw new Error("httpReqRefToken - Invalid token response from Salesforce");
}
return result; // includes access_token
} catch (err) {
logger.error(`httpReqRefToken - Error: ${err.message}`);
throw err; // Propagate error so calling function can handle it
}
}
export default httpReqRefToken;

View File

@ -0,0 +1,45 @@
import request from "request";
import logger from "../config/logger.js";
async function httpReqValidateAuth(
orgId,
access_token,
instance_url,
refresh_token
) {
const getRequest = () => {
return new Promise((resolve, reject) => {
try {
request.get(
{
headers: {
'Authorization': `Bearer ${access_token}`,
},
url: `${instance_url}/services/data/v58.0/query?q=select+id+from+user+limit+1`,
},
(error, res) => {
resolve(res);
}
);
} catch (error) {
reject("error in validateAuth.httpReq1", error);
}
});
};
// Await the postRequest promise
try {
const result = await getRequest();
if(result.statusCode === 200 || result.statusCode === 201){
logger.info("Access Token is Valid - validateAuth.httpReq < ==")
return true;
}
logger.info("Access Token is Invalid - validateAuth.httpReq < ==")
return false;
} catch (err) {
logger.error("Error in validateAuth.httpReq2", err);
}
}
export default httpReqValidateAuth;

View File

@ -0,0 +1,79 @@
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());
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) {
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 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})
}
}
export {
authValidator
}

13
src/models/authModel.js Normal file
View File

@ -0,0 +1,13 @@
import mongoose from "mongoose";
const authSchema = new mongoose.Schema({
org_id: String,
access_token: String,
refresh_token: String,
instance_url: String,
is_sandbox: Boolean,
is_connected: Boolean
}, { timestamps: true })
const AuthModel = new mongoose.model("org_auths", authSchema);
export default AuthModel;

148
src/models/vaModel.js Normal file
View File

@ -0,0 +1,148 @@
import mongoose from "mongoose";
const { Schema, model, Types } = mongoose;
/* ---------------------- User Details Schema ---------------------- */
const userDetailsSchema = new Schema({
firstName: {
type: String,
},
lastName: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
dob: {
type: String,
},
age: {
type: String,
},
address: {
type: String,
},
password: {
type: String,
},
bankName: {
type: String,
},
accountHolderName: {
type: String,
},
accountNumber: {
type: String,
},
routingNumber: {
type: String,
},
});
/* ---------------------- Tracking Schema ---------------------- */
const trackingSchema = new Schema({
type: {
type: String,
enum: ["SCREENSHOT", "VIDEO"],
default: "SCREENSHOT",
},
duration: {
type: String,
default: "5M",
},
});
/* ---------------------- Project Schema ---------------------- */
const projectSchema = new Schema({
name: {
type: String,
required: true,
},
totalLoggedHours: {
type: Number,
},
vaAssigned: [
{
type: Types.ObjectId,
ref: "VA",
},
],
});
/* ---------------------- Client Schema ---------------------- */
const clientSchema = new Schema({
userDetails: {
name: {
type: String,
},
email: {
type: String,
},
},
vaIds: [
{
type: Types.ObjectId,
ref: "VA",
},
],
projectIds: [
{
type: Types.ObjectId,
ref: "Project",
},
],
totalLoggedHours: {
type: Number,
},
});
/* ---------------------- logged Schema ---------------------- */
const loggedSchema = new Schema({
loggedType: {
type: String,
enum: ["LOGIN", "LOGOUT"],
default: "LOGIN"
},
time: {
type: Date,
default: Date.now
},
vaId: {
type: Types.ObjectId,
ref: "VA"
}
})
/* ---------------------- VA Schema ---------------------- */
const vaSchema = new Schema(
{
userDetails: userDetailsSchema,
tracking: trackingSchema,
clientIds: [
{
type: Types.ObjectId,
ref: "Client",
},
],
schedule: {
type: String,
},
status: {
type: String,
enum: ["PARTTIME", "FULLTIME", "INACTIVE"],
default: "FULLTIME",
},
},
{ timestamps: true }
);
/* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema);
const ProjectModel = model("Project", projectSchema);
const LoggedModel = model("Logged", loggedSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel };

View File

@ -0,0 +1,65 @@
import AuthModel from "../models/authModel.js";
class AuthRepository {
async create(data) {
try {
const newAuth = new AuthModel({...data});
const response = await newAuth.save();
return response;
} catch (error) {
throw error;
}
}
async update(filter, update) {
try {
const response = await AuthModel.updateOne(filter, { $set: { ...update } });
return response.acknowledged;
} catch (error) {
throw error;
}
}
async fetch() {
try {
const response = await AuthModel.find({}, {_id: 1, org_id: 1, instance_url: 1, createdAt: 1, updatedAt: 1});
if (!response) throw new Error("No data found");
return response;
} catch (error) {
throw error;
}
}
async fetchById(id) {
try {
const response = await AuthModel.findOne({ _id: id });
if (!response) return false;
return response;
} catch (error) {
throw error;
}
}
async fetchByOrgId(orgId) {
try {
const response = await AuthModel.findOne({ org_id: orgId });
if (!response) return false;
return response;
} catch (error) {
throw error;
}
}
async remove(id) {
try {
const response = await AuthModel.deleteOne({ _id: id });
if (!response) throw new Error("No data found");
return response;
} catch (error) {
throw error;
}
}
}
export default AuthRepository;

View File

@ -0,0 +1,8 @@
function resObject(isSuccess, {...arg}) {
return {
success: isSuccess,
...arg
}
}
export default resObject;

8
src/routes/index.js Normal file
View File

@ -0,0 +1,8 @@
import express from"express";
import v1Routes from"./v1/index.js";
const apiRoutes = express.Router()
apiRoutes.use('/v1', v1Routes);
export default apiRoutes;

176
src/routes/v1/auth.html Normal file
View File

@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>SalesForce Authentication</title>
<script>
let baseUrl = `https://smashing-albacore-engaging.ngrok-free.app/api/v1/auth_callback/callback`;
</script>
</head>
<style>
@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");
* {
font-family: "Poppins", sans-serif;
font-weight: 400;
font-style: normal;
padding: 0;
margin: 0;
}
html,
body {
width: 100vw;
height: 100vh;
}
.parent {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 10px;
font-size: 20px;
position: relative;
justify-content: center;
}
body {
background-color: #def2fe;
display: flex;
align-items: center;
justify-content: center;
}
.loader {
width: 300px;
height: 100px;
display: flex;
justify-content: center;
align-items: flex-end;
}
.bar {
width: 20px;
height: 10px;
margin: 0 5px;
background-color: #3498db;
border-radius: 5px;
animation: loading-wave-animation 1s ease-in-out infinite;
}
.bar:nth-child(2) {
animation-delay: 0.1s;
}
.bar:nth-child(3) {
animation-delay: 0.2s;
}
.bar:nth-child(4) {
animation-delay: 0.3s;
}
@keyframes loading-wave-animation {
0% {
height: 10px;
}
50% {
height: 50px;
}
100% {
height: 10px;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
callback();
});
async function callback() {
let newDiv = document.createElement("div");
let centerDiv = document.getElementById("center");
let parentDiv = document.getElementById("parent");
try {
let url = window.location.search;
console.log(url);
let searchParams = new URLSearchParams(url);
let code = searchParams.get("code");
let object = searchParams.get("state");
try {
var dataObj = JSON.parse(object);
} catch (error) {}
dataObj.code = code;
console.log(baseUrl)
await fetch(baseUrl, {
method: "POST",
headers: new Headers({
"Content-Type": "application/json;charset=utf-8",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Origin": "*",
}),
body: JSON.stringify(dataObj),
})
.then((res) => {
console.log("Response is", res);
if (res.status === 201) {
return { message: "Connected", success: true };
}
return res.json();
})
.then((res) => {
let timeOut = 0;
if (res.success) {
const success = `<i class="fa fa-rocket" style="color:rgb(51, 119, 221);font-size:100px; position:absolute;right:-30%;top:-60%"></i>
<p style="color: rgb(51, 119, 221); font-size:80px;">${res.message}</p>`;
timeOut = 3;
newDiv.innerHTML = success;
} else {
const error = `<i class="fa fa-rocket" style="color:rgb(51, 119, 221);font-size:100px; position:absolute;right:-30%;top:-60%"></i>
<p style="color: rgb(51, 119, 221); font-size:80px;">Hmm...</p>
<div><p>Error while authenticating, Closing tab in 5 secs</p><p><b>${res.message}</p></b></div>`;
timeOut = 5;
newDiv.innerHTML = error;
}
if (centerDiv.firstChild != null) {
while (centerDiv.firstChild) centerDiv.firstChild.remove();
centerDiv.appendChild(newDiv);
} else parentDiv.appendChild(newDiv);
setTimeout(() => window.close(), timeOut * 1000);
})
.catch((error) => {
console.log("Error Connecting", error)
newDiv.innerText = "Internal server error.Error code:c2";
centerDiv.appendChild(newDiv);
setTimeout(() => window.close(), 10000);
});
} catch (err) {
if (err.message) {
const error = `<i class="fa fa-rocket" style="color:rgb(51, 119, 221);font-size:100px; position:absolute;right:-30%;top:-60%"></i>
<p style="color: rgb(51, 119, 221); font-size:80px;">Hmm...</p>
<div><p>Error while authenticating,<p><b>${err.message}</b></p></div>`;
newDiv.innerHTML = error;
} else {
newDiv.innerText =
"Unknown error.Please contact administrator.Error code:c32";
}
if (centerDiv != null) {
while (centerDiv.firstChild) centerDiv.firstChild.remove();
centerDiv.appendChild(newDiv);
} else parentDiv.appendChild(newDiv);
setTimeout(() => window.close(), 10000);
}
}
</script>
<body>
<div id="parent" class="parent">
<div id="center" class="loader">
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
import express from"express";
const authRoutes = express.Router();
import { auth_callback, auth_validate, auth_connected, auth_disconnect } from "../../controllers/auth.controllers.js";
authRoutes.post("/callback", auth_callback)
authRoutes.get("/authenticated/:orgId", auth_validate)
authRoutes.get("/connected/:orgId", auth_connected)
authRoutes.get("/disconnect/:orgId", auth_disconnect)
export default authRoutes;

View File

@ -0,0 +1 @@
<script>window.open(`${process.env.BASE_URI}/api/v1/call`)</script>

28
src/routes/v1/index.js Normal file
View File

@ -0,0 +1,28 @@
import path from 'path';
import authRoutes from './auth.routes.js';
import vaRoutes from './va.routes.js';
import express from "express";
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const v1Routes = express.Router()
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
v1Routes.get("/auth", function(req, res) {
res.sendFile(path.join(__dirname, '/auth.html'));
});
v1Routes.use("/auth_callback", authRoutes);
v1Routes.use("/va", vaRoutes);
v1Routes.get("/", function(req, res) {
res.json({"connected": true})
})
// v1Routes.use("/form_submit", authValidator, formRoutes);
export default v1Routes;

View File

@ -0,0 +1,32 @@
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 } from "../../controllers/va.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
vaRoutes.get("/login", va_login_callback); // VA Callback for login VA
vaRoutes.post("/register", va_register); // Register a new VA
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
vaRoutes.get("/clients/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID
vaRoutes.get("/clients/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback
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
vaRoutes.get("/:id/log/project/:projectID", 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
vaRoutes.get("/search", va_filter_search);
export default vaRoutes;

56
src/server.js Normal file
View File

@ -0,0 +1,56 @@
import express from 'express';
import mongoose from "mongoose";
import bodyParser from "body-parser";
import dotenv from "dotenv";
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import apiRoutes from './routes/index.js'
dotenv.config()
const app = express();
app.set("view engine", "ejs");
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('/varegister', (req, res) => {
res.render('va_register', { title: "VA Registration" });
});
app.get('/health', (req, res) => {
res.status(201).send({ status: "Server is healthy" });
})
// Express App Listening
app.listen(PORT, () => {
mongoose
.connect(
process.env.MONGO_URI,
)
.then(() => console.log("mongodb connected"))
.catch((err) => {
console.log(err.message);
process.exit(1);
});
console.log(`Express started on http://localhost:${PORT}`);
});

View File

@ -0,0 +1,70 @@
import logger from "../config/logger.js";
class AuthService {
constructor(AuthRepository) {
this.authRepository = AuthRepository;
}
async create(data) {
try {
const response = await this.authRepository.create(data);
return response;
} catch (error) {
logger.log("Auth Service errorA" + error);
}
}
async update(filter, update) {
try {
const response = await this.authRepository.update(filter, update);
return response;
} catch (error) {
logger.log("Auth Service errorB" + error);
}
}
async fetchAll() {
try {
console.log("calling FetchAll")
const response = await this.authRepository.fetch();
if (!response.length) throw new error("No data found");
return response;
} catch (error) {
logger.log("Auth Service errorC" + error);
}
}
async fetchById(id) {
try {
const response = await this.authRepository.fetchById(id);
if (!response) {
return false;
}
return response;
} catch (error) {
logger.log("Auth Service errorD" + error);
}
}
async fetchByOrgId(orgId) {
try {
const response = await this.authRepository.fetchByOrgId(orgId);
if (!response) {
return false;
}
return response;
} catch (error) {
logger.log("Auth Service errorE" + error);
}
}
async remove(authId) {
try {
const response = await this.authRepository.remove(authId);
return response;
} catch (error) {
logger.log("Auth Service errorF" + error);
}
}
}
export default AuthService;

135
views/va_register.ejs Normal file
View File

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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">
</head>
<body>
<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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Custom JS -->
<script src="/public/js/va-register.js"></script>
</body>
</html>