Compare commits

..

No commits in common. "5f043ed80a5416992f69e4501ba06852bd9c32ff" and "287d078b4901a8e0a584aae302d92b589794d80b" have entirely different histories.

22 changed files with 49 additions and 442 deletions

18
package-lock.json generated
View File

@ -40,8 +40,7 @@
"uglify-js": "^3.19.3",
"uuid": "^11.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"wireguard-tools": "^0.1.0"
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@babel/cli": "^7.26.4",
@ -6199,12 +6198,6 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -9345,15 +9338,6 @@
"node": ">= 12.0.0"
}
},
"node_modules/wireguard-tools": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/wireguard-tools/-/wireguard-tools-0.1.0.tgz",
"integrity": "sha512-oBnnSB6wXGEAB1ZVPfbfpX8WlHNw7wv45r13ILX73felVnrGpMxMjFcKGAsMeRfBOGzXcW0LgAWe+T53dRcI+g==",
"license": "MIT",
"dependencies": {
"lodash.mergewith": "^4.6.2"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",

View File

@ -45,8 +45,7 @@
"uglify-js": "^3.19.3",
"uuid": "^11.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"wireguard-tools": "^0.1.0"
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@babel/cli": "^7.26.4",

View File

@ -492,11 +492,9 @@ async function validateStep(step) {
} else if (step === 1) {
const email = document.getElementById('email').value;
const companyName = document.getElementById('company-name').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!validateEmail(email) && !companyName && !password && !confirmPassword) {
if (!validateEmail(email) && !companyName) {
isValid = false;
showError('Fields are Required', null, step);
showError('Both Fields are Required', null, step);
let requiredFields1 = form.querySelectorAll('[required]');
requiredFields1.forEach(field => {
if (!field.value.trim()) {
@ -524,21 +522,6 @@ async function validateStep(step) {
return isValid;
}
if (password.length < 8) {
showError('Password must be at least 8 characters long', 'password', step);
isValid = false;
}
if (password !== confirmPassword) {
document.getElementById('password-match-error').style.display = 'block';
document.getElementById('confirmPassword').classList.add('field-error');
showError('Passwords do not match', 'confirmPassword', step);
isValid = false;
} else {
document.getElementById('password-match-error').style.display = 'none';
document.getElementById('confirmPassword').classList.remove('field-error');
}
// Validate other required fields
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => {
@ -629,7 +612,6 @@ function saveStepData(step) {
case 1:
formData.userDetails.name = document.getElementById('company-name').value;
formData.userDetails.email = document.getElementById('email').value;
formData.userDetails.password = document.getElementById('password').value;
break;
case 2:
formData.userDetails.address = document.getElementById('address').value;
@ -1973,51 +1955,3 @@ function preventInvalidAccountHolderChars(event) {
event.preventDefault();
return false;
}
// Function to generate a strong password
function generatePassword() {
const length = 12;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
let password = "";
// Ensure at least one of each required character type
password += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)];
password += "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)];
password += "0123456789"[Math.floor(Math.random() * 10)];
password += "!@#$%^&*"[Math.floor(Math.random() * 8)];
// Fill the rest randomly
for (let i = password.length; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
// Shuffle the password
password = password.split('').sort(() => 0.5 - Math.random()).join('');
// Set the password field
document.getElementById('password').value = password;
document.getElementById('confirmPassword').value = password;
// Trigger input events to update UI
document.getElementById('password').dispatchEvent(new Event('input'));
document.getElementById('confirmPassword').dispatchEvent(new Event('input'));
}
// Function to toggle password visibility
function togglePasswordVisibility() {
const passwordField = document.getElementById('password');
const confirmField = document.getElementById('confirmPassword');
const toggleButton = document.querySelector('.password-toggle');
if (passwordField.type === 'password') {
passwordField.type = 'text';
confirmField.type = 'text';
toggleButton.textContent = 'Hide Password';
} else {
passwordField.type = 'password';
confirmField.type = 'password';
toggleButton.textContent = 'Show Password';
}
}

View File

@ -25,10 +25,10 @@ async function va_register_client(req, res) {
try {
const payload = req.body || {};
const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.name || !userDetails.password) {
if (!userDetails || !userDetails.email || !userDetails.name) {
return res.status(400).json({
success: false,
message: "Missing required fields: name, email, and password are required"
message: "Missing required fields: name and email are required"
});
}

View File

@ -71,7 +71,7 @@ async function project_add_sf(req, res) {
hourlyRate: hourlyRate || '0',
clientId: clientObjectId,
vaAssigned: vaAssigned,
dedicatedHours: dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"],
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowed: isFlexibleAllowed || true,

View File

@ -74,7 +74,7 @@ async function update_project_sf(req, res) {
// Update dedicated hours if provided
if (dedicatedHours !== undefined) {
updateData.dedicatedHours = dedicatedHours || ["0", "0", "0", "0", "0", "0", "0"];
updateData.dedicatedHours = parseFloat(dedicatedHours) || 0;
}
// Update total logged hours if provided

View File

@ -1,7 +1,6 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog) {
@ -54,7 +53,7 @@ async function closePreviousSession(vaId, lastLog) {
"time": logoutLog.time,
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -1,6 +1,5 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Apply for leave
async function va_leave_apply_function(req, res) {
@ -165,7 +164,7 @@ async function va_leave_apply_function(req, res) {
"rejectionReason": ""
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -1,5 +1,4 @@
import { VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
async function va_payslip_data_function(req, res) {
try {
@ -12,7 +11,7 @@ async function va_payslip_data_function(req, res) {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip/${vaId}`;
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',

View File

@ -2,7 +2,7 @@ async function va_payroll_schedule_function(req, res) {
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/payroll-schedule`;
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',

View File

@ -1,6 +1,5 @@
import bcrypt from "bcrypt";
import { VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Register a new VA
async function va_register(req, res) {
@ -54,7 +53,7 @@ async function va_register(req, res) {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
delete created.userDetails._id;
created.userDetails._id = created._id; // Include VA's MongoDB ID
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -1,5 +1,4 @@
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) {
@ -198,7 +197,7 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
"time": logoutLog.time,
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -374,7 +373,7 @@ async function va_project_log_callback(req, res) {
"time": createdLog.time,
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -472,7 +471,7 @@ async function va_project_log_callback(req, res) {
"time": createdLog.time,
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -534,7 +533,7 @@ async function va_project_log_callback(req, res) {
"time": createdLog.time,
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -1,6 +1,5 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Adjust missing hours by creating new records
async function va_timesheet_adjustment_function(req, res) {
@ -156,7 +155,7 @@ async function va_timesheet_adjustment_function(req, res) {
"adjustedHours": formatTime(adjustedSeconds)
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -1,5 +1,4 @@
import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js";
// Approve or reject leave
async function va_tips_manage_function(req, res) {
@ -39,7 +38,7 @@ async function va_tips_manage_function(req, res) {
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
}
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -117,7 +116,7 @@ async function va_tips_delete(req, res) {
// "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
// }
const response = await authFetch(salesforceUrl, {
const response = await fetch(salesforceUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',

View File

@ -101,8 +101,8 @@ const projectSchema = new Schema({
}
],
dedicatedHours: {
type: Array,
default: ["0", "0", "0", "0", "0", "0", "0"]
type: String,
default: "0"
},
status: {
type: String,

View File

@ -13,14 +13,13 @@ import { project_add_sf } from "../../controllers/project/projectAddSf.controlle
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
import { authValidator } from "../../middlewares/auth.middleware.js";
import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js"
import { createWireGuardConnection, disconnectWireGuardConnection } from '../../vpnHandler/vpn.js';
const v1Routes = express.Router()
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
v1Routes.get("/auth", function (req, res) {
v1Routes.get("/auth", function(req, res) {
res.sendFile(path.join(__dirname, '/auth.html'));
});
@ -35,50 +34,9 @@ v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
v1Routes.post("/password/reset", process_reset);
v1Routes.get("/", function (req, res) {
res.json({ "connected": true })
v1Routes.get("/", function(req, res) {
res.json({"connected": true})
})
v1Routes.post("/vpn/connect", (req, res) => {
try {
const data = createWireGuardConnection();
res.json({
success: true,
message: "VPN connection created",
...data
});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
});
v1Routes.post("/vpn/disconnect", (req, res) => {
try {
const { ip } = req.body;
if (!ip) {
return res.status(400).json({
success: false,
error: "IP address is required"
});
}
const result = disconnectWireGuardConnection(ip);
if (result && result.success) {
return res.status(200).json(result);
}
return res.status(400).json({success: false, error: result.message});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
});
// v1Routes.use("/form_submit", authValidator, formRoutes);
export default v1Routes;

View File

@ -114,6 +114,10 @@ app.get('/clientregister/:sf_token?', (req, res) => {
app.get('/auth/password/reset-form', render_reset_form);
app.get('/health', (req, res) => {
res.status(201).send({ status: "Server is healthy" });
})
app.get("/client/login", (req, res) => {
res.render("client_login")
})
@ -503,8 +507,12 @@ app.get("/forgot-password", (req, res) => {
res.render("forgot_password")
})
app.get("/health", (req, res) => {
return res.status(200).json({ "success": true, "message": "Server is Healthy" })
app.get("/heath", () => {
if (Math.round(Math.random()) === 1) {
return res.status(200).json({ "success": true, "message": "Server is Healthy" })
} else {
return res.status(403).json({ "success": false, "message": "Server is not Healthy" })
}
})
app.use((req, res) => {

View File

@ -3,7 +3,6 @@ import bcrypt from "bcrypt";
import { VAModel, ClientModel } from "../models/vaModel.js";
import logger from "../config/logger.js";
import axios from "axios";
import authFetch from "../utils/authFetch.js";
class PasswordResetService {
constructor(passwordResetRepository) {
@ -170,19 +169,14 @@ class PasswordResetService {
try {
const userTypeText = userType === 'va' ? 'VA' : 'CLIENT';
const res = await authFetch(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: {
email: email,
url: resetLink,
userType: userTypeText
}
})
await axios.post(`${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/forgot-password`, {
data: {
email: email,
url: resetLink,
userType: userTypeText
}
});
} catch (error) {
logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`);
throw new Error("Failed to send reset email");

View File

@ -18,7 +18,7 @@ class VaClientService {
}
// Sanitize and prepare data
const clientData = await this.sanitizeClientData(data);
const clientData = this.sanitizeClientData(data);
const response = await this.clientRepository.create(clientData);
if(response) {
@ -263,17 +263,13 @@ class VaClientService {
}
// Helper method to sanitize data
async sanitizeClientData(payload) {
sanitizeClientData(payload) {
const { userDetails } = payload;
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
return {
userDetails: {
name: userDetails.name,
email: userDetails.email.toLowerCase(),
password: hashedPassword,
address: userDetails.address || null,
city: userDetails.city || null,
state: userDetails.state || null,

View File

@ -1,68 +0,0 @@
import fetch from "node-fetch"; // or global fetch in Node 18+
let preOptions = {
token: ''
}
const getAccessToken = async () => {
try {
const response = await fetch(`${process.env.SF_DOCTRACKER_API_URL}services/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SF_CLIENT_ID,
client_secret: process.env.SF_CLIENT_SECRET,
}),
});
console.log(response)
const access_token = (await response.json()).access_token;
preOptions.token = access_token;
return access_token;
} catch (error) {
console.error('Error fetching access token:', error);
throw error;
}
};
export const authFetch = async (url, options = {}) => {
let isvalid = false;
if (preOptions.token != "" && preOptions.token != null && preOptions.token != undefined) {
isvalid = await fetch(`${process.env.SF_DOCTRACKER_TOKEN_VALIDATION_URL}services/apexrest/docva/health}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${preOptions.token}`,
},
});
const a = JSON.stringify(isvalid.body);
console.log(a);
}
if (isvalid.status !== 200) {
await getAccessToken();
}
return fetch(url, {
...options,
headers: {
...(options.headers || {}),
"Content-Type": "application/json",
Authorization: `Bearer ${preOptions.token}`,
},
});
};
export default authFetch;
export { getAccessToken }

View File

@ -1,173 +0,0 @@
import fs from "fs";
import { execSync } from "child_process";
const WG_CONF = "/etc/wireguard/wg0.conf";
const PEER_DB = "/etc/wireguard/peers.json";
const CLIENT_DIR = "/etc/wireguard/clients";
const SERVER_PUBLIC_KEY = process.env.SERVER_PUBLIC_KEY || "jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U=";
const SERVER_IP = process.env.SERVER_PUBLIC_IP || "35.95.209.161";
function loadDb() {
try {
return JSON.parse(fs.readFileSync(PEER_DB, "utf8"));
} catch {
const initial = { lastIp: 1, active: {}, free: [] };
fs.writeFileSync(PEER_DB, JSON.stringify(initial, null, 2));
return initial;
}
}
function saveDb(db) {
fs.writeFileSync(PEER_DB, JSON.stringify(db, null, 2));
}
function allocateIp(db) {
if (db.free.length > 0) {
return db.free.shift(); // reuse old IP
}
db.lastIp++;
if (db.lastIp >= 255) db.lastIp = 2;
return `10.0.0.${db.lastIp}`;
}
function generateKeyPair() {
const privateKey = execSync("wg genkey").toString().trim();
const publicKey = execSync(`echo ${privateKey} | wg pubkey`).toString().trim();
return { privateKey, publicKey };
}
// function addPeerToServer(publicKey, ip) {
// const ipLast = ip.split(".")[3];
// const peerEntry = `
// [Peer]
// PublicKey = ${publicKey}
// AllowedIPs = 10.0.0.${ipLast}/32
// `;
// fs.appendFileSync(WG_CONF, peerEntry);
// // apply config instantly
// // execSync(`wg syncconf wg0 <(wg-quick strip wg0)`, { shell: "/bin/bash" });
// execSync(`wg-quick strip wg0 > /tmp/wg0.tmp`);
// execSync(`wg syncconf wg0 /tmp/wg0.tmp`);
// }
function addPeerToServer(publicKey, ip) {
const peerEntry = `
[Peer]
PublicKey = ${publicKey}
AllowedIPs = ${ip}/32
`;
fs.appendFileSync(WG_CONF, peerEntry);
execSync("wg-quick strip wg0 > /tmp/wg0.tmp");
execSync("wg syncconf wg0 /tmp/wg0.tmp");
execSync("rm -f /tmp/wg0.tmp");
execSync(
`wg set wg0 peer ${publicKey} allowed-ips ${ip}/32`,
{ stdio: "inherit" }
);
}
function generateClientConf(privateKey, ip) {
return `
[Interface]
PrivateKey = ${privateKey}
Address = ${ip}/32
DNS = 1.1.1.1
[Peer]
PublicKey = ${SERVER_PUBLIC_KEY}
Endpoint = ${SERVER_IP}:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
`;
}
export function createWireGuardConnection() {
let db = loadDb();
// 1. allocate IP
const ip = allocateIp(db);
// 2. generate keys
const { privateKey, publicKey } = generateKeyPair();
// 3. update server config
addPeerToServer(publicKey, ip);
// 4. update DB
db.active[ip] = publicKey;
saveDb(db);
// 5. prepare client.conf
const clientConf = generateClientConf(privateKey, ip);
// (optional) save it
if (!fs.existsSync(CLIENT_DIR)) fs.mkdirSync(CLIENT_DIR);
fs.writeFileSync(`${CLIENT_DIR}/client-${ip}.conf`, clientConf);
// 6. return to Electron app
return { ip, publicKey, clientConf };
}
// -------- Disconnect VPN PART START -----------
const removePeerFromConf = (publicKey) => {
const conf = fs.readFileSync(WG_CONF, "utf8");
const peerRegex = new RegExp(
`\\n\\[Peer\\][\\s\\S]*?PublicKey = ${publicKey}[\\s\\S]*?(?=\\n\\[Peer\\]|$)`,
"g"
);
const updated = conf.replace(peerRegex, "\n");
fs.writeFileSync(WG_CONF, updated);
}
function syncWireGuard() {
execSync("wg-quick strip wg0 > /tmp/wg0.tmp");
execSync("wg syncconf wg0 /tmp/wg0.tmp");
execSync("rm -f /tmp/wg0.tmp");
}
export function disconnectWireGuardConnection(ip) {
const db = loadDb();
const publicKey = db.active[ip];
if (!db.active[ip]) {
return { success: false, message: "VPN already disconnected" };
}
// 1️⃣ Remove peer from live kernel
execSync(`wg set wg0 peer ${publicKey} remove`);
// 2️⃣ Remove from config file
removePeerFromConf(publicKey);
// 3️⃣ Sync config
syncWireGuard();
// 4️⃣ Update DB (free IP)
delete db.active[ip];
db.free.push(ip);
saveDb(db);
return {
success: true,
message: "VPN disconnected",
ip
};
}

View File

@ -111,24 +111,6 @@
<label for="email" class="input-label">Email *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="password" class="input-label">Create Password *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="confirmPassword" class="input-label">Confirm Password *</label>
</div>
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
<div class="password-actions">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
</div>
</div>
<div class="form-actions">
<div></div> <!-- Empty div for spacing -->
<button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
@ -260,7 +242,7 @@
<div class="error-message" id="step-4-error-message"></div>
</div>
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
We will use the details below in the agreement that we will send to your prior to starting the project.
We will use the details below in the agreement that we will send to your prior to entering the project.
</p>
<form id="signatory-form">
<div class="form-group">
@ -340,7 +322,7 @@
</div>
</div>
<div class="payment-option" onclick="selectPaymentOption('bank')">
<h4>Bank Account (ACH)</h4>
<h4>Bank Account (ACR)</h4>
<p>Direct bank transfer</p>
</div>
<div id="bank-details" class="payment-details">