This commit is contained in:
commit
48276d7161
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
logs
|
||||
.env
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"watch": [
|
||||
"src",
|
||||
"views"
|
||||
],
|
||||
"ext": "js,ejs",
|
||||
"exec": "node src/server.js"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
function resObject(isSuccess, {...arg}) {
|
||||
return {
|
||||
success: isSuccess,
|
||||
...arg
|
||||
}
|
||||
}
|
||||
|
||||
export default resObject;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
<script>window.open(`${process.env.BASE_URI}/api/v1/call`)</script>
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue