Added new API

This commit is contained in:
Aakash Mittal 2025-11-12 19:50:20 +05:30
parent f6e9eeffa9
commit 7fac56b5ec
24 changed files with 1552 additions and 320 deletions

147
package-lock.json generated
View File

@ -30,6 +30,7 @@
"mongodb": "^6.12.0", "mongodb": "^6.12.0",
"mongoose": "^8.9.3", "mongoose": "^8.9.3",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"nodemailer": "^7.0.10",
"nodemon": "^3.1.0", "nodemon": "^3.1.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"puppeteer": "^24.10.1", "puppeteer": "^24.10.1",
@ -44,11 +45,8 @@
"@babel/cli": "^7.26.4", "@babel/cli": "^7.26.4",
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0", "@babel/preset-env": "^7.26.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^9.2.1", "babel-loader": "^9.2.1",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"terser-webpack-plugin": "^5.3.11", "terser-webpack-plugin": "^5.3.11",
"webpack": "^5.97.1", "webpack": "^5.97.1",
"webpack-cli": "^6.0.1", "webpack-cli": "^6.0.1",
@ -2534,44 +2532,6 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.24.4",
"caniuse-lite": "^1.0.30001702",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/aws-sign2": { "node_modules/aws-sign2": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -4661,20 +4621,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fresh": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -6553,25 +6499,6 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -6694,6 +6621,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemailer": {
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz",
"integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz",
@ -6750,16 +6686,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nth-check": { "node_modules/nth-check": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
@ -7222,42 +7148,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -8306,16 +8196,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": { "node_modules/source-map-support": {
"version": "0.5.21", "version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -8482,13 +8362,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",

View File

@ -34,6 +34,7 @@
"mongodb": "^6.12.0", "mongodb": "^6.12.0",
"mongoose": "^8.9.3", "mongoose": "^8.9.3",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"nodemailer": "^7.0.10",
"nodemon": "^3.1.0", "nodemon": "^3.1.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"puppeteer": "^24.10.1", "puppeteer": "^24.10.1",

View File

@ -0,0 +1,151 @@
/* auth.css - custom styles for centered card UI */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-decoration: none;
}
/* Page background and centering */
.page-center {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc; /* light gray */
padding: 24px;
box-sizing: border-box;
}
/* Card */
.auth-card {
width: 100%;
max-width: 420px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.06);
padding: 28px;
box-sizing: border-box;
border: 1px solid rgba(15, 23, 42, 0.04);
}
/* Header area */
.auth-header {
text-align: center;
margin-bottom: 18px;
}
.auth-icon {
font-size: 40px;
display: inline-block;
line-height: 1;
margin-bottom: 8px;
}
.auth-title {
font-size: 20px;
font-weight: 600;
color: #0f172a;
margin: 6px 0;
}
.auth-sub {
color: #6b7280;
font-size: 13px;
}
/* Alerts */
.alert {
padding: 10px 12px;
border-radius: 8px;
font-size: 13px;
margin-bottom: 14px;
border: 1px solid transparent;
}
.alert-error {
background: #fff5f5;
color: #b91c1c;
border-color: #fca5a5;
}
.alert-success {
background: #f0fdf4;
color: #059669;
border-color: #86efac;
}
/* Form controls */
.form-row {
margin-bottom: 12px;
}
.form-input {
width: 100%;
padding: 12px 14px;
border-radius: 8px;
border: 1px solid #e6e9ee;
background: #fbfbfd;
font-size: 14px;
color: #0f172a;
box-sizing: border-box;
transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.form-input:focus {
outline: none;
border-color: #2563eb; /* blue */
box-shadow: 0 0 0 4px rgba(37,99,235,0.08);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 10px 14px;
border-radius: 8px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.08s ease, box-shadow 0.12s ease, background-color 0.08s ease;
}
.btn-primary {
background: #000;
color: #fff;
}
.btn-primary:hover { transform: translateY(-1px); background: #111; }
.btn-ghost {
background: #ffffff;
color: #111827;
border: 1px solid #e6e9ee;
}
.btn-ghost:hover { background: #fbfbfb; transform: translateY(-1px); }
/* Secondary small link */
.small-link {
display: block;
text-align: center;
margin-top: 12px;
font-size: 13px;
color: #2563eb;
text-decoration: none;
}
/* Utility */
.text-center { text-align: center; }
.mb-4 { margin-bottom: 16px; }
.mt-4 { margin-top: 16px; }
/* Responsive tweaks */
@media (max-width: 480px) {
.auth-card { padding: 20px; border-radius: 10px; }
.auth-icon { font-size: 36px; }
.auth-title { font-size: 18px; }
}

View File

@ -20,7 +20,7 @@ body {
.sidebar { .sidebar {
display: none; display: none;
flex-direction: column; flex-direction: column;
width: 344px; width: 280px;
background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0); background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0);
padding: 24px; padding: 24px;
border-radius: 14px; border-radius: 14px;
@ -186,6 +186,7 @@ body {
.form-group { .form-group {
margin-bottom: 16px; margin-bottom: 16px;
position: relative;
} }
.input-container { .input-container {
@ -369,6 +370,9 @@ body {
font-size: 12px; font-size: 12px;
color: #6b7280; color: #6b7280;
margin-top: 4px; margin-top: 4px;
margin-left: 12px;
position: absolute;
bottom: 0;
} }
.row { .row {
@ -440,7 +444,6 @@ body {
color: #070065; color: #070065;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
text-decoration: underline;
} }
.form-text { .form-text {

View File

@ -142,10 +142,66 @@ const statesByCountry = {
'England', 'Scotland', 'Wales', 'Northern Ireland' 'England', 'Scotland', 'Wales', 'Northern Ireland'
], ],
'AU': [ 'AU': [
'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia' 'New South Wales', 'Queensland', 'South Australia', 'Tasmania', 'Victoria', 'Western Australia',
'Australian Capital Territory', 'Northern Territory'
],
'IN': [
'Andhra Pradesh', 'Arunachal Pradesh', 'Assam', 'Bihar', 'Chhattisgarh', 'Goa', 'Gujarat',
'Haryana', 'Himachal Pradesh', 'Jharkhand', 'Karnataka', 'Kerala', 'Madhya Pradesh',
'Maharashtra', 'Manipur', 'Meghalaya', 'Mizoram', 'Nagaland', 'Odisha', 'Punjab', 'Rajasthan',
'Sikkim', 'Tamil Nadu', 'Telangana', 'Tripura', 'Uttar Pradesh', 'Uttarakhand', 'West Bengal',
'Andaman and Nicobar Islands', 'Chandigarh', 'Dadra and Nagar Haveli and Daman and Diu',
'Delhi', 'Jammu and Kashmir', 'Ladakh', 'Lakshadweep', 'Puducherry'
],
'DE': [
'Baden-Württemberg', 'Bavaria', 'Berlin', 'Brandenburg', 'Bremen', 'Hamburg', 'Hesse',
'Lower Saxony', 'Mecklenburg-Vorpommern', 'North Rhine-Westphalia', 'Rhineland-Palatinate',
'Saarland', 'Saxony', 'Saxony-Anhalt', 'Schleswig-Holstein', 'Thuringia'
],
'FR': [
'Auvergne-Rhône-Alpes', 'Bourgogne-Franche-Comté', 'Brittany', 'Centre-Val de Loire', 'Corsica',
'Grand Est', 'Hauts-de-France', 'Île-de-France', 'Normandy', 'Nouvelle-Aquitaine', 'Occitanie',
'Pays de la Loire', 'Provence-Alpes-Côte d’Azur'
],
'IT': [
'Abruzzo', 'Basilicata', 'Calabria', 'Campania', 'Emilia-Romagna', 'Friuli Venezia Giulia',
'Lazio', 'Liguria', 'Lombardy', 'Marche', 'Molise', 'Piedmont', 'Sardinia', 'Sicily',
'Trentino-Alto Adige', 'Tuscany', 'Umbria', 'Aosta Valley', 'Veneto'
],
'ES': [
'Andalusia', 'Aragon', 'Asturias', 'Balearic Islands', 'Basque Country', 'Canary Islands',
'Cantabria', 'Castile and León', 'Castile-La Mancha', 'Catalonia', 'Extremadura', 'Galicia',
'La Rioja', 'Madrid', 'Murcia', 'Navarre', 'Valencia'
],
'BR': [
'Acre', 'Alagoas', 'Amapá', 'Amazonas', 'Bahia', 'Ceará', 'Distrito Federal', 'Espírito Santo',
'Goiás', 'Maranhão', 'Mato Grosso', 'Mato Grosso do Sul', 'Minas Gerais', 'Pará', 'Paraíba',
'Paraná', 'Pernambuco', 'Piauí', 'Rio de Janeiro', 'Rio Grande do Norte', 'Rio Grande do Sul',
'Rondônia', 'Roraima', 'Santa Catarina', 'São Paulo', 'Sergipe', 'Tocantins'
],
'MX': [
'Aguascalientes', 'Baja California', 'Baja California Sur', 'Campeche', 'Chiapas', 'Chihuahua',
'Coahuila', 'Colima', 'Durango', 'Guanajuato', 'Guerrero', 'Hidalgo', 'Jalisco', 'Mexico State',
'Michoacán', 'Morelos', 'Nayarit', 'Nuevo León', 'Oaxaca', 'Puebla', 'Querétaro', 'Quintana Roo',
'San Luis Potosí', 'Sinaloa', 'Sonora', 'Tabasco', 'Tamaulipas', 'Tlaxcala', 'Veracruz',
'Yucatán', 'Zacatecas'
],
'CN': [
'Anhui', 'Beijing', 'Chongqing', 'Fujian', 'Gansu', 'Guangdong', 'Guangxi', 'Guizhou', 'Hainan',
'Hebei', 'Heilongjiang', 'Henan', 'Hubei', 'Hunan', 'Inner Mongolia', 'Jiangsu', 'Jiangxi',
'Jilin', 'Liaoning', 'Ningxia', 'Qinghai', 'Shaanxi', 'Shandong', 'Shanghai', 'Shanxi',
'Sichuan', 'Tianjin', 'Tibet', 'Xinjiang', 'Yunnan', 'Zhejiang'
],
'JP': [
'Aichi', 'Akita', 'Aomori', 'Chiba', 'Ehime', 'Fukui', 'Fukuoka', 'Fukushima', 'Gifu', 'Gunma',
'Hiroshima', 'Hokkaido', 'Hyogo', 'Ibaraki', 'Ishikawa', 'Iwate', 'Kagawa', 'Kagoshima', 'Kanagawa',
'Kochi', 'Kumamoto', 'Kyoto', 'Mie', 'Miyagi', 'Miyazaki', 'Nagano', 'Nagasaki', 'Nara', 'Niigata',
'Oita', 'Okayama', 'Okinawa', 'Osaka', 'Saga', 'Saitama', 'Shiga', 'Shimane', 'Shizuoka', 'Tochigi',
'Tokushima', 'Tokyo', 'Tottori', 'Toyama', 'Wakayama', 'Yamagata', 'Yamaguchi', 'Yamanashi'
] ]
}; };
// Function to navigate to any step (for tab navigation) // Function to navigate to any step (for tab navigation)
function goToStep(step) { function goToStep(step) {
// Only allow navigation to completed steps or the next step // Only allow navigation to completed steps or the next step
@ -601,57 +657,67 @@ async function populateStates(countryCode, selectElementId) {
option.textContent = state; option.textContent = state;
selectElement.appendChild(option); selectElement.appendChild(option);
}); });
// const selectElement = document.getElementById(selectElementId);
// // Clear existing options except the first empty one
// while (selectElement.options.length > 1) {
// selectElement.remove(1);
// }
// // Fetch states from API
// const states = await fetchStates(countryCode);
// // Add states as options
// states.forEach(state => {
// const option = document.createElement('option');
// option.value = state.code || state.name;
// option.textContent = state.name;
// selectElement.appendChild(option);
// });
} }
// Function to detect user location // Function to detect user location
async function detectLocation() { async function detectLocation() {
// Show loading indicators // Show loading indicators
document.getElementById('state-loading').style.display = 'block'; const stateLoader = document.getElementById('state-loading');
if (stateLoader) stateLoader.style.display = 'block';
try { try {
const response = await fetch('https://ipapi.co/json/'); const response = await fetch('https://ipapi.co/json/');
const data = await response.json(); const data = await response.json();
// Hide loading indicators // Hide loading indicators
document.getElementById('state-loading').style.display = 'none'; if (stateLoader) stateLoader.style.display = 'none';
// const countryCode = data.country_code || 'US';
const supportedCountries = Object.keys(statesByCountry);
const countryCode = supportedCountries.includes(data.country_code)
? data.country_code
: 'US';
const region = data.region || '';
const city = data.city || '';
const zip = data.postal || '';
// Populate city and state based on detected location // Populate city and state based on detected location
document.getElementById('city').value = data.city || ''; const cityInput = document.getElementById('city');
document.getElementById('zip').value = data.postal || ''; const zipInput = document.getElementById('zip');
if (cityInput) cityInput.value = city;
if (zipInput) zipInput.value = zip;
// Populate states dropdown for the detected country // Populate states dropdown for the detected country
await populateStates(data.country_code, 'state'); await populateStates(countryCode, 'state');
await populateStates(countryCode, 'billing-state');
// Set the detected state
if (data.region) { const stateSelect = document.getElementById('state');
document.getElementById('state').value = data.region; const billingStateSelect = document.getElementById('billing-state');
document.getElementById('state').dispatchEvent(new Event('change'));
if (stateSelect && region) {
const matchedState = Array.from(stateSelect.options).find(
opt => opt.text.toLowerCase() === region.toLowerCase()
);
if (matchedState) {
stateSelect.value = matchedState.value;
stateSelect.dispatchEvent(new Event('change'));
}
} }
// Also populate billing states dropdown if (billingStateSelect && region) {
await populateStates(data.country_code, 'billing-state'); const matchedBillingState = Array.from(billingStateSelect.options).find(
opt => opt.text.toLowerCase() === region.toLowerCase()
);
if (matchedBillingState) {
billingStateSelect.value = matchedBillingState.value;
billingStateSelect.dispatchEvent(new Event('change'));
}
}
} catch (error) { } catch (error) {
console.error('Error detecting location:', error); console.error('Error detecting location:', error);
// Hide loading indicators // Hide loading indicators
document.getElementById('state-loading').style.display = 'none'; if (stateLoader) stateLoader.style.display = 'none';
// Fallback to US states if detection fails // Fallback to US states if detection fails
await populateStates('US', 'state'); await populateStates('US', 'state');

View File

@ -107,10 +107,6 @@
}, 500); }, 500);
}); });
// Password strength indicator
document.getElementById('password').addEventListener('input', function() {
updatePasswordStrength(this.value);
});
// Add input event listeners to handle label animation // Add input event listeners to handle label animation
document.querySelectorAll('.input-field').forEach(input => { document.querySelectorAll('.input-field').forEach(input => {
@ -529,9 +525,6 @@
// Trigger input events to update UI // Trigger input events to update UI
document.getElementById('password').dispatchEvent(new Event('input')); document.getElementById('password').dispatchEvent(new Event('input'));
document.getElementById('confirmPassword').dispatchEvent(new Event('input')); document.getElementById('confirmPassword').dispatchEvent(new Event('input'));
// Update password strength indicator
updatePasswordStrength(password);
} }
// Function to toggle password visibility // Function to toggle password visibility
@ -551,36 +544,6 @@
} }
} }
// Function to update password strength indicator
function updatePasswordStrength(password) {
const strengthBar = document.getElementById('password-strength-bar');
// Reset classes
strengthBar.className = 'password-strength-bar';
if (password.length === 0) {
strengthBar.style.width = '0';
} else if (password.length < 6) {
strengthBar.classList.add('password-weak');
} else if (password.length < 10) {
strengthBar.classList.add('password-medium');
} else {
// Check for complexity
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
const complexityScore = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecial].filter(Boolean).length;
if (complexityScore >= 3) {
strengthBar.classList.add('password-strong');
} else {
strengthBar.classList.add('password-medium');
}
}
}
// Function to submit the form // Function to submit the form
async function submitForm() { async function submitForm() {
// Validate current step // Validate current step
@ -645,89 +608,3 @@
} }
} }
// Initialize form interactions
// document.addEventListener('DOMContentLoaded', function() {
// // Password strength indicator
// document.getElementById('password').addEventListener('input', function() {
// updatePasswordStrength(this.value);
// });
// // Add input event listeners to handle label animation
// document.querySelectorAll('.input-field').forEach(input => {
// input.addEventListener('input', function() {
// if (this.value) {
// this.nextElementSibling.style.top = '4px';
// this.nextElementSibling.style.fontSize = '12px';
// this.nextElementSibling.style.transform = 'none';
// } else {
// this.nextElementSibling.style.top = '50%';
// this.nextElementSibling.style.fontSize = '14px';
// this.nextElementSibling.style.transform = 'translateY(-50%)';
// }
// });
// input.addEventListener('focus', function() {
// this.nextElementSibling.style.top = '4px';
// this.nextElementSibling.style.fontSize = '12px';
// this.nextElementSibling.style.transform = 'none';
// });
// input.addEventListener('blur', function() {
// if (!this.value) {
// this.nextElementSibling.style.top = '50%';
// this.nextElementSibling.style.fontSize = '14px';
// this.nextElementSibling.style.transform = 'translateY(-50%)';
// }
// });
// });
// // Handle select field label animation
// document.querySelectorAll('.select-field').forEach(select => {
// select.addEventListener('change', function() {
// if (this.value) {
// this.nextElementSibling.style.top = '4px';
// this.nextElementSibling.style.fontSize = '12px';
// this.nextElementSibling.style.transform = 'none';
// } else {
// this.nextElementSibling.style.top = '50%';
// this.nextElementSibling.style.fontSize = '14px';
// this.nextElementSibling.style.transform = 'translateY(-50%)';
// }
// });
// select.addEventListener('focus', function() {
// this.nextElementSibling.style.top = '4px';
// this.nextElementSibling.style.fontSize = '12px';
// this.nextElementSibling.style.transform = 'none';
// });
// select.addEventListener('blur', function() {
// if (!this.value) {
// this.nextElementSibling.style.top = '50%';
// this.nextElementSibling.style.fontSize = '14px';
// this.nextElementSibling.style.transform = 'translateY(-50%)';
// }
// });
// });
// // Step navigation for desktop
// document.querySelectorAll('.step').forEach(step => {
// step.addEventListener('click', function() {
// const stepNum = parseInt(this.getAttribute('data-step'));
// if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) {
// goToStep(stepNum);
// }
// });
// });
// // Step navigation for mobile
// document.querySelectorAll('.mobile-step').forEach(step => {
// step.addEventListener('click', function() {
// const stepNum = parseInt(this.getAttribute('data-step'));
// if (stepNum <= currentStep || stepNum === getMaxCompletedStep() + 1) {
// goToStep(stepNum);
// }
// });
// });
// });

View File

@ -0,0 +1,122 @@
import PasswordResetRepository from "../repository/passwordReset.repository.js";
import PasswordResetService from "../services/passwordReset.service.js";
import EmailService from "../services/email.service.js";
import logger from "../config/logger.js";
const passwordResetService = new PasswordResetService(
new PasswordResetRepository(),
new EmailService({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
from: process.env.EMAIL_FROM,
})
);
// VA: Request password reset
async function va_request_password_reset(req, res) {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({ success: false, message: "Email is required" });
}
const result = await passwordResetService.requestPasswordReset(email, 'va');
logger.info(`VA password reset requested for: ${email}`);
return res.status(200).json(result);
} catch (error) {
logger.error(`VA PasswordResetController - Request error: ${error.message}`);
return res.status(500).json({ success: false, message: "Failed to process password reset request" });
}
}
// Client: Request password reset
async function client_request_password_reset(req, res) {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({ success: false, message: "Email is required" });
}
const result = await passwordResetService.requestPasswordReset(email, 'client');
logger.info(`Client password reset requested for: ${email}`);
return res.status(200).json(result);
} catch (error) {
logger.error(`Client PasswordResetController - Request error: ${error.message}`);
return res.status(500).json({ success: false, message: "Failed to process password reset request" });
}
}
// Render reset password form (shared for both VA and Client)
async function render_reset_form(req, res) {
try {
const { token, userType } = req.query;
if (!token || !userType) {
return res.render('auth/reset-password-error', { error: 'Reset token or user type is missing' });
}
// Validate token
const validation = await passwordResetService.validateResetToken(token, userType);
if (!validation.valid) {
return res.render('auth/reset-password-error', { error: validation.message });
}
// Render reset form with token
res.render('auth/reset-password-form', { token, userType, error: null });
} catch (error) {
logger.error(`PasswordResetController - Render form error: ${error.message}`);
res.render('auth/reset-password-error', { error: 'Failed to load reset form' });
}
}
// Process password reset form submission (shared for both VA and Client)
async function process_reset(req, res) {
try {
const { token, password, confirmPassword, userType } = req.body;
if (!token || !password || !confirmPassword || !userType) {
return res.render('auth/reset-password-form', {
token, userType, error: 'All fields are required'
});
}
if (password !== confirmPassword) {
return res.render('auth/reset-password-form', {
token, userType, error: 'Passwords do not match'
});
}
if (password.length < 6) {
return res.render('auth/reset-password-form', {
token, userType, error: 'Password must be at least 6 characters long'
});
}
// Reset password
await passwordResetService.resetPassword(token, password, userType);
// Render success page
res.render('auth/reset-password-success');
} catch (error) {
logger.error(`PasswordResetController - Process reset error: ${error.message}`);
res.render('auth/reset-password-form', {
token: req.body.token,
userType: req.body.userType,
error: error.message
});
}
}
export {
va_request_password_reset,
client_request_password_reset,
render_reset_form,
process_reset
};

View File

@ -0,0 +1,151 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
// Adjust missing hours by creating new records
async function va_timesheet_adjustment_function(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
const { projectId, date } = req.params;
const { loginTime, logoutTime, reason } = req.body;
if (!projectId || !date) {
return res.status(400).json({ success: false, message: "Project ID and date are required" });
}
if (!loginTime || !logoutTime) {
return res.status(400).json({ success: false, message: "Login time and logout time are required" });
}
// Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({
_id: projectId,
vaAssigned: vaId
});
if (!project) {
return res.status(404).json({ success: false, message: "Project not found or VA not assigned to this project" });
}
// Parse and validate times
const adjustmentDate = new Date(date);
const loginDateTime = new Date(loginTime);
const logoutDateTime = new Date(logoutTime);
// Ensure times are within the target date
const startOfDay = new Date(adjustmentDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(adjustmentDate);
endOfDay.setHours(23, 59, 59, 999);
if (loginDateTime < startOfDay || loginDateTime > endOfDay ||
logoutDateTime < startOfDay || logoutDateTime > endOfDay) {
return res.status(400).json({
success: false,
message: "Login and logout times must be within the specified date"
});
}
if (loginDateTime >= logoutDateTime) {
return res.status(400).json({
success: false,
message: "Login time must be before logout time"
});
}
// Check for existing logs that might conflict with this adjustment
const existingLogs = await LoggedModel.find({
vaId,
projectId,
time: {
$gte: loginDateTime,
$lte: logoutDateTime
}
});
if (existingLogs.length > 0) {
return res.status(400).json({
success: false,
message: "There are existing logs in the specified time range. Please choose a different time range."
});
}
// Create adjustment logs
const loginLog = new LoggedModel({
loggedType: "LOGIN",
time: loginDateTime,
vaId,
vaEmail,
projectId,
isAdjustment: true,
adjustmentReason: reason
});
const logoutLog = new LoggedModel({
loggedType: "LOGOUT",
time: logoutDateTime,
vaId,
vaEmail,
projectId,
isAdjustment: true,
adjustmentReason: reason
});
// Save both logs in transaction
await loginLog.save();
await logoutLog.save();
// Calculate adjusted hours
const adjustedSeconds = (logoutDateTime - loginDateTime) / 1000;
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
};
const response = {
success: true,
message: "Timesheet adjustment completed successfully",
data: {
adjustment: {
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
adjustedHoursDecimal: (adjustedSeconds / 3600).toFixed(2),
reason: reason
},
logs: {
loginLogId: loginLog._id,
logoutLogId: logoutLog._id
}
}
};
return res.status(201).json(response);
} catch (error) {
console.error("Timesheet adjustment error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_timesheet_adjustment_function };

View File

@ -0,0 +1,199 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
async function va_timesheet_summary_function(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
const { projectId, date } = req.params;
if (!projectId || !date) {
return res.status(400).json({ success: false, message: "Project ID and date are required" });
}
// Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({
_id: projectId,
vaAssigned: vaId
});
if (!project) {
return res.status(404).json({ success: false, message: "Project not found or VA not assigned to this project" });
}
// Calculate date range for the specific day
let targetDate;
try {
// Try parsing as ISO date (YYYY-MM-DD)
targetDate = new Date(date);
if (isNaN(targetDate.getTime())) {
// Try parsing as YYYY-MM-DD format specifically
const [year, month, day] = date.split('-');
targetDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
if (isNaN(targetDate.getTime())) {
return res.status(400).json({ success: false, message: "Invalid date format. Use YYYY-MM-DD" });
}
} catch (error) {
return res.status(400).json({ success: false, message: "Invalid date format" });
}
const startOfDay = new Date(targetDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(targetDate);
endOfDay.setHours(23, 59, 59, 999);
// Get all logs for this VA, project, and date
const logs = await LoggedModel.find({
vaId,
projectId,
time: { $gte: startOfDay, $lte: endOfDay }
}).sort({ time: 1 }).lean();
// Calculate work sessions and totals
let totalWorkSeconds = 0;
let firstLoginTime = null;
let lastLogoutTime = null;
const sessions = [];
const loginStack = [];
for (const log of logs) {
const logTime = new Date(log.time);
if (log.loggedType === "LOGIN") {
loginStack.push(logTime);
// Set first login time
if (!firstLoginTime || logTime < firstLoginTime) {
firstLoginTime = logTime;
}
} else if (log.loggedType === "LOGOUT" && lastLogin) {
if (loginStack.length > 0) {
// Pop the last login from stack
const lastLogin = loginStack.pop();
const sessionSeconds = (logTime - lastLogin) / 1000;
totalWorkSeconds += sessionSeconds;
// Set last logout time
if (!lastLogoutTime || logTime > lastLogoutTime) {
lastLogoutTime = logTime;
}
sessions.push({
login: lastLogin,
logout: logTime,
durationSeconds: sessionSeconds
});
} else {
console.log(`Orphaned LOGOUT at ${logTime.toISOString()} - no matching LOGIN`);
}
}
}
// Handle open session (LOGIN without LOGOUT)
if (loginStack.length > 0) {
for (const lastLogin of loginStack) {
const sessionSeconds = (endOfDay - lastLogin) / 1000;
totalWorkSeconds += sessionSeconds;
sessions.push({
login: lastLogin,
logout: null,
durationSeconds: sessionSeconds,
isOpen: true
});
}
}
// Convert seconds to time format
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
};
const formatTimeDetailed = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) {
return `${h}h ${m}m ${s}s`;
} else if (m > 0) {
return `${m}m ${s}s`;
} else {
return `${s}s`;
}
};
// Calculate dedicated hours and pending hours
const dedicatedHours = project.dedicatedHours || 0;
const totalWorkedHours = totalWorkSeconds / 3600; // Convert to hours
const pendingHours = Math.max(0, dedicatedHours - totalWorkedHours);
const response = {
success: true,
data: {
date: targetDate.toISOString().split('T')[0],
project: {
id: project._id,
name: project.name,
dedicatedHours: dedicatedHours
},
va: {
id: vaUser._id,
name: vaUser.userDetails?.name || 'Unknown',
email: vaEmail
},
summary: {
totalWorkedHours: formatTime(totalWorkSeconds),
totalWorkedHoursDetailed: formatTimeDetailed(totalWorkSeconds),
totalWorkedHoursDecimal: totalWorkedHours.toFixed(4),
totalWorkedMinutes: (totalWorkSeconds / 60).toFixed(2),
dedicatedHours: dedicatedHours,
pendingHours: pendingHours.toFixed(4),
firstLoginTime: firstLoginTime ? firstLoginTime.toISOString() : null,
lastLogoutTime: lastLogoutTime ? lastLogoutTime.toISOString() : null
},
sessions: sessions.map(session => ({
login: session.login.toISOString(),
logout: session.logout ? session.logout.toISOString() : null,
duration: formatTime(session.durationSeconds),
durationDetailed: formatTimeDetailed(session.durationSeconds),
durationSeconds: session.durationSeconds,
isOpen: session.isOpen || false
})),
}
};
return res.status(200).json(response);
} catch (error) {
console.error("Timesheet summary error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_timesheet_summary_function };

View File

@ -86,6 +86,14 @@ const projectSchema = new Schema({
ref: "VA", ref: "VA",
}, },
], ],
clientId: {
type: Types.ObjectId,
ref: "Client"
},
dedicatedHours: {
type: Number,
default: 0
}
}); });
/* ---------------------- Client Details Schema ---------------------- */ /* ---------------------- Client Details Schema ---------------------- */
@ -194,7 +202,12 @@ const clientSchema = new Schema({
refreshToken: { refreshToken: {
type: String, type: String,
default: null default: null
},
passwordResetToken: {
type: String,
default: null
} }
}); });
@ -255,6 +268,10 @@ const vaSchema = new Schema(
refreshToken: { refreshToken: {
type: String, type: String,
default: null default: null
},
passwordResetToken: {
type: String,
default: null
} }
}, },
{ timestamps: true } { timestamps: true }
@ -304,11 +321,49 @@ const vaWorkTrackSchema = new Schema({
{ timestamps: true } { timestamps: true }
) )
const passwordResetSchema = new mongoose.Schema({
email: {
type: String,
required: true,
lowercase: true,
index: true
},
token: {
type: String,
required: true,
unique: true,
index: true
},
userType: {
type: String,
required: true,
enum: ['va', 'client'],
index: true
},
expiresAt: {
type: Date,
required: true,
index: true,
expires: 3600 // TTL index for 1 hour
},
used: {
type: Boolean,
default: false
}
}, {
timestamps: true
});
passwordResetSchema.index({ email: 1, used: 1 });
passwordResetSchema.index({ token: 1, used: 1, expiresAt: 1 });
passwordResetSchema.index({ userType: 1, email: 1 });
/* ---------------------- Model Exports ---------------------- */ /* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema); const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema); const ClientModel = model("Client", clientSchema);
const ProjectModel = model("Project", projectSchema); const ProjectModel = model("Project", projectSchema);
const LoggedModel = model("TrackLog", loggedSchema); const LoggedModel = model("TrackLog", loggedSchema);
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel }; export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel };

View File

@ -0,0 +1,66 @@
import { PasswordResetModel } from "../models/vaModel.js";
class PasswordResetRepository {
async create(data) {
try {
const newReset = new PasswordResetModel({...data});
const response = await newReset.save();
return response;
} catch (error) {
throw error;
}
}
async findByToken(token) {
try {
const response = await PasswordResetModel.findOne({ token });
return response;
} catch (error) {
throw error;
}
}
async findByEmail(email) {
try {
const response = await PasswordResetModel.findOne({
email: email.toLowerCase()
});
return response;
} catch (error) {
throw error;
}
}
async deleteByEmail(email) {
try {
const response = await PasswordResetModel.deleteMany({
email: email.toLowerCase()
});
return response;
} catch (error) {
throw error;
}
}
async deleteByToken(token) {
try {
const response = await PasswordResetModel.deleteOne({ token });
return response;
} catch (error) {
throw error;
}
}
async deleteExpiredTokens() {
try {
const response = await PasswordResetModel.deleteMany({
expiresAt: { $lt: new Date() }
});
return response;
} catch (error) {
throw error;
}
}
}
export default PasswordResetRepository;

View File

@ -6,23 +6,18 @@ import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.co
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js"; import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.controller.js"; import { client_refresh_token_callback } from "../../controllers/client/clientRefreshToken.controller.js";
import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js";
// clients routes // clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/register", va_register_client); // Register a new VA
clientRoutes.post("/login", va_login_client); // Login Client clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.get("/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC) clientRoutes.get("/:clientID", va_clients_callback); // VA Clients Callback for getting all clients or specific client by ID (NC)
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
// // API endpoint (for frontend to call)
// clientRoutes.post("/forgot-password", va_client_request_password_reset);
// // EJS rendering routes
// clientRoutes.get("/reset-form", va_client_render_reset_form);
// clientRoutes.post("/reset", va_client_process_reset);

View File

@ -8,6 +8,8 @@ import express from "express";
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname } from 'path';
import { process_reset } from "../../controllers/passwordReset.controllers.js";
const v1Routes = express.Router() const v1Routes = express.Router()
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -22,6 +24,8 @@ v1Routes.use("/auth_callback", authRoutes);
v1Routes.use("/va", vaRoutes); v1Routes.use("/va", vaRoutes);
v1Routes.use("/client", clientRoutes); v1Routes.use("/client", clientRoutes);
v1Routes.post("/password/reset", process_reset);
v1Routes.get("/", function(req, res) { v1Routes.get("/", function(req, res) {
res.json({"connected": true}) res.json({"connected": true})
}) })

View File

@ -17,6 +17,9 @@ import { va_log_track_work_callback } from "../../controllers/va/vaLog/vaLogTrac
import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js"; import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWorkReport.js";
import { va_projects_report_function } from "../../controllers/va/vaProjects/vaProjectReport.controller.js"; import { va_projects_report_function } from "../../controllers/va/vaProjects/vaProjectReport.controller.js";
import { va_projects_log_list_function } from "../../controllers/va/vaProjects/vaProjectLogList.controller.js"; import { va_projects_log_list_function } from "../../controllers/va/vaProjects/vaProjectLogList.controller.js";
import { va_request_password_reset } from "../../controllers/passwordReset.controllers.js";
import { va_timesheet_summary_function } from "../../controllers/va/vaLog/vaTimesheetAdjustmentSummary.controller.js";
import { va_timesheet_adjustment_function } from "../../controllers/va/vaLog/vaTimesheetAdjustment.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA vaRoutes.get("/", va_callback); // VA Callback for getting active VA
@ -24,6 +27,7 @@ vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA vaRoutes.post("/register", va_register); // Register a new VA
vaRoutes.post("/forgot-password", va_request_password_reset);
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
@ -33,7 +37,8 @@ vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC) vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC)
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
vaRoutes.post("/timesheet/adjustment/:projectId/:date", authValidator, va_timesheet_adjustment_function); // Get Timesheet Adjustment for VA on specific project and date
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC) // Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)
vaRoutes.get("/search", va_filter_search); vaRoutes.get("/search", va_filter_search);
@ -55,12 +60,6 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba
// VA filter search: supports query params `name`, `email`, `from`, `to` // VA filter search: supports query params `name`, `email`, `from`, `to`
// // API endpoint (for frontend to call)
// vaRoutes.post("/forgot-password", va_request_password_reset);
// // EJS rendering routes
// vaRoutes.get("/reset-form", va_render_reset_form);
// vaRoutes.post("/reset", va_process_reset);
export default vaRoutes; export default vaRoutes;

View File

@ -1,17 +1,18 @@
import dotenv from "dotenv";
dotenv.config()
import express from 'express'; import express from 'express';
import mongoose from "mongoose"; import mongoose from "mongoose";
import bodyParser from "body-parser"; import bodyParser from "body-parser";
import dotenv from "dotenv";
import cors from 'cors'; import cors from 'cors';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import apiRoutes from './routes/index.js' import apiRoutes from './routes/index.js'
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
dotenv.config()
const app = express(); const app = express();
app.set("view engine", "ejs"); app.set("view engine", "ejs");
const PORT = process.env.PORT || 8000; const PORT = process.env.PORT || 8000;
@ -40,6 +41,8 @@ app.get('/clientregister', (req, res) => {
res.render('va_client_register', { title: "Client Registration" }); res.render('va_client_register', { title: "Client Registration" });
}); });
app.get('/auth/password/reset-form', render_reset_form);
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.status(201).send({ status: "Server is healthy" }); res.status(201).send({ status: "Server is healthy" });
}) })

View File

@ -0,0 +1,303 @@
// services/email.service.js
import nodemailer from "nodemailer";
import logger from "../config/logger.js";
class EmailService {
constructor() {
this.transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || "smtp.gmail.com",
port: process.env.SMTP_PORT || 587,
// secure: false, // Use TLS
auth: {
user: process.env.SMTP_USER || "aakashtest2007@gmail.com",
pass: process.env.SMTP_PASS || "licb yqet bfad kkqy",
},
// Gmail specific settings
tls: {
rejectUnauthorized: false,
ciphers: 'SSLv3'
},
connectionTimeout: 30000, // Increased timeout for Gmail
socketTimeout: 30000,
greetingTimeout: 30000,
});
this.verifyConnection();
}
setupDevelopmentFallback() {
logger.warn(' Using development fallback - emails will be logged but not sent');
this.transporter = {
verify: async () => {
logger.info('Development mode: Gmail SMTP verification skipped');
return true;
},
sendMail: async (mailOptions) => {
logger.info('Development mode: Email would be sent via Gmail');
logger.info(` To: ${mailOptions.to}`);
logger.info(` Subject: ${mailOptions.subject}`);
logger.info(` From: ${mailOptions.from}`);
// Simulate Gmail sending delay
await new Promise(resolve => setTimeout(resolve, 1000));
return {
messageId: 'gmail-dev-' + Date.now(),
response: '250 2.0.0 OK - Development mode'
};
}
};
}
async verifyConnection() {
try {
await this.transporter.verify();
logger.info(`Gmail SMTP connection verified successfully`);
logger.info(`Gmail account: ${process.env.SMTP_USER}`);
} catch (error) {
logger.error(`Gmail SMTP connection failed: ${error.message}`);
this.logGmailTroubleshootingTips(error);
}
}
logGmailTroubleshootingTips(error) {
logger.info(`Gmail Troubleshooting Tips:`);
logger.info(` 1. Enable 2-Factor Authentication in Google Account`);
logger.info(` 2. Generate App Password (16 characters) - NOT your regular password`);
logger.info(` 3. Allow less secure apps: https://myaccount.google.com/lesssecureapps`);
logger.info(` 4. Unlock Captcha: https://accounts.google.com/DisplayUnlockCaptcha`);
logger.info(` 5. Check if port 587 is blocked by firewall`);
logger.info(` 6. Error details: ${error.message}`);
}
async sendEmail(emailData) {
try {
const mailOptions = {
from: process.env.EMAIL_FROM,
to: emailData.to,
subject: emailData.subject,
html: emailData.html,
// Gmail specific headers
headers: {
'X-Priority': '1',
'X-Mailer': 'Node.js Password Reset',
'List-Unsubscribe': `<mailto:${process.env.SUPPORT_EMAIL}?subject=Unsubscribe>`,
}
};
logger.info(`Sending email via Gmail to: ${emailData.to}`);
const result = await this.transporter.sendMail(mailOptions);
if (result.response && result.response.includes('250')) {
logger.info(`Email sent successfully via Gmail to: ${emailData.to}`);
logger.debug(`Gmail Response: ${result.response}`);
} else {
logger.info(`Email processed via Gmail to: ${emailData.to}`);
}
return result;
} catch (error) {
logger.error(`Gmail email send failed: ${error.message}`);
// Enhanced error handling for Gmail specific issues
if (error.message.includes('Invalid login')) {
throw new Error('Gmail authentication failed. Use App Password, not regular password.');
} else if (error.message.includes('ECONNREFUSED')) {
throw new Error('Gmail connection refused. Check firewall/port 587.');
} else if (error.message.includes('rate limit')) {
throw new Error('Gmail rate limit exceeded. Try again later.');
} else if (error.message.includes('quota')) {
throw new Error('Gmail sending quota exceeded.');
} else {
throw new Error(`Gmail error: ${error.message}`);
}
}
}
// Enhanced test email for Gmail
async sendTestEmail(toEmail = "test@example.com") {
try {
const testEmail = {
to: toEmail,
subject: "Gmail Test - Password Reset System Working",
html: this.generateGmailTestTemplate(),
};
const result = await this.sendEmail(testEmail);
return {
success: true,
message: "Test email sent successfully via Gmail",
messageId: result.messageId,
response: result.response
};
} catch (error) {
return {
success: false,
message: error.message
};
}
}
generateGmailTestTemplate() {
const currentTime = new Date().toLocaleString();
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 600px;
margin: 20px auto;
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #4285F4 0%, #34A853 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 28px;
font-weight: 300;
}
.content {
padding: 40px 30px;
}
.success-badge {
background: #34A853;
color: white;
padding: 10px 20px;
border-radius: 20px;
display: inline-block;
margin-bottom: 20px;
font-weight: bold;
}
.details-card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin: 25px 0;
border-left: 4px solid #4285F4;
}
.footer {
background: #f1f3f4;
padding: 20px;
text-align: center;
color: #5f6368;
font-size: 12px;
}
.gmail-logo {
font-size: 24px;
font-weight: bold;
color: #4285F4;
margin-bottom: 10px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-top: 20px;
}
.info-item {
background: white;
padding: 15px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div style="font-size: 48px; margin-bottom: 10px;">✉️</div>
<h1>Gmail SMTP Test Successful</h1>
<p>Your password reset system is now configured with Gmail</p>
</div>
<div class="content">
<div class="success-badge"> TEST PASSED</div>
<h2>Gmail Configuration Verified</h2>
<p>Your email service is working perfectly with Gmail SMTP!</p>
<div class="details-card">
<div class="gmail-logo">Gmail</div>
<h3 style="margin-top: 0;">Configuration Details</h3>
<div class="info-grid">
<div class="info-item">
<strong>🔧 SMTP Host</strong><br>
${process.env.SMTP_HOST}
</div>
<div class="info-item">
<strong>📮 Sender</strong><br>
${process.env.EMAIL_FROM}
</div>
<div class="info-item">
<strong>🕐 Time</strong><br>
${currentTime}
</div>
<div class="info-item">
<strong>⚡ Status</strong><br>
Active & Ready
</div>
</div>
</div>
<div style="background: #e8f5e8; padding: 15px; border-radius: 8px; border-left: 4px solid #34A853;">
<h4 style="margin-top: 0; color: #2e7d32;">🎉 Ready for Production</h4>
<p style="margin-bottom: 0;">Your password reset emails will now be delivered reliably via Gmail.</p>
</div>
</div>
<div class="footer">
<p>This is a test email from your Password Reset System</p>
<p>Powered by Gmail SMTP • ${currentTime}</p>
</div>
</div>
</body>
</html>
`;
}
// Check Gmail sending limits and status
getGmailStatus() {
return {
provider: 'Gmail',
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
fromEmail: process.env.EMAIL_FROM,
limits: {
daily: '500 emails per day',
rate: '100 emails per hour',
attachment: '25MB per email'
},
requirements: [
'2-Factor Authentication enabled',
'App Password used (16 characters)',
'Less secure apps access may need enabling'
]
};
}
}
export default EmailService;

View File

@ -0,0 +1,260 @@
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import { VAModel, ClientModel } from "../models/vaModel.js";
import logger from "../config/logger.js";
class PasswordResetService {
constructor(passwordResetRepository, emailService) {
this.passwordResetRepository = passwordResetRepository;
this.emailService = emailService;
}
async requestPasswordReset(email, userType) {
try {
// Determine which model to use
const userModel = userType === 'va' ? VAModel : ClientModel;
// Check if user exists
const user = await userModel.findOne({ "userDetails.email": email.toLowerCase() });
if (!user) {
logger.info(`Password reset requested for non-existent ${userType}: ${email}`);
return {
success: true,
message: "If the email exists, a reset link has been sent"
};
}
// Delete any existing reset tokens for this email
await this.passwordResetRepository.deleteByEmail(email);
// Generate JWT token
const token = jwt.sign(
{
email: email.toLowerCase(),
userType: userType,
type: 'password_reset'
},
process.env.JWT_RESET_SECRET,
{ expiresIn: process.env.RESET_TOKEN_EXPIRY || '1h' }
);
// Create reset record
const resetData = {
email: email.toLowerCase(),
token,
userType: userType,
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
used: false
};
await this.passwordResetRepository.create(resetData);
// Store token in user document
user.passwordResetToken = token;
await user.save();
// Generate reset link
const resetLink = `${process.env.BACKEND_URL}/auth/password/reset-form?token=${token}&userType=${userType}`;
// Send email
await this.sendResetEmail(email, resetLink, userType);
logger.info(`Password reset token created for ${userType}: ${email}`);
return {
success: true,
message: "If the email exists, a reset link has been sent"
};
} catch (error) {
logger.error(`PasswordReset Service - requestPasswordReset error for ${userType}: ${error.message}`);
throw error;
}
}
async validateResetToken(token, userType) {
try {
// Verify token
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_RESET_SECRET);
} catch (error) {
return { valid: false, message: "Invalid or expired token" };
}
// Validate user type
if (decoded.userType !== userType) {
return { valid: false, message: "Invalid token for this user type" };
}
// Check database
const resetRecord = await this.passwordResetRepository.findByToken(token);
if (!resetRecord || resetRecord.used || resetRecord.expiresAt < new Date()) {
return { valid: false, message: "Invalid or expired token" };
}
return {
valid: true,
email: decoded.email,
userType: decoded.userType
};
} catch (error) {
logger.error(`PasswordReset Service - validateResetToken error: ${error.message}`);
return { valid: false, message: "Token validation failed" };
}
}
async resetPassword(token, newPassword, userType) {
try {
// Verify token
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_RESET_SECRET);
} catch (error) {
throw new Error("Invalid or expired reset token");
}
// Validate user type
if (decoded.userType !== userType) {
throw new Error("Invalid reset token for this user type");
}
// Check if token exists in database and is valid
const resetRecord = await this.passwordResetRepository.findByToken(token);
if (!resetRecord || resetRecord.used || resetRecord.expiresAt < new Date()) {
throw new Error("Invalid or expired reset token");
}
// Determine which model to use
const userModel = userType === 'va' ? VAModel : ClientModel;
// Check if user exists and has matching token
const user = await userModel.findOne({
"userDetails.email": decoded.email,
passwordResetToken: token
});
if (!user) {
throw new Error("User not found or invalid token");
}
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 12);
// Update user password
user.userDetails.password = hashedPassword;
user.passwordResetToken = null;
await user.save();
// Mark token as used
resetRecord.used = true;
await resetRecord.save();
// Send confirmation email
await this.sendPasswordChangedEmail(decoded.email, userType);
logger.info(`Password successfully reset for ${userType}: ${decoded.email}`);
return {
success: true,
message: "Password has been reset successfully"
};
} catch (error) {
logger.error(`PasswordReset Service - resetPassword error for ${userType}: ${error.message}`);
throw error;
}
}
async sendResetEmail(email, resetLink, userType) {
try {
const userTypeText = userType === 'va' ? 'Virtual Assistant' : 'Client';
const emailData = {
to: email,
subject: `${userTypeText} - Password Reset Request`,
html: this.generateResetEmailTemplate(resetLink, userTypeText),
};
await this.emailService.sendEmail(emailData);
logger.info(`Password reset email sent to ${userType}: ${email}`);
} catch (error) {
logger.error(`PasswordReset Service - sendResetEmail error for ${userType}: ${error.message}`);
throw new Error("Failed to send reset email");
}
}
async sendPasswordChangedEmail(email, userType) {
try {
const userTypeText = userType === 'va' ? 'Virtual Assistant' : 'Client';
const emailData = {
to: email,
subject: `${userTypeText} - Password Changed Successfully`,
html: this.generatePasswordChangedEmailTemplate(userTypeText),
};
await this.emailService.sendEmail(emailData);
logger.info(`Password changed confirmation sent to ${userType}: ${email}`);
} catch (error) {
logger.error(`PasswordReset Service - sendPasswordChangedEmail error for ${userType}: ${error.message}`);
// Don't throw error here as password was already reset
}
}
generateResetEmailTemplate(resetLink, userType) {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.button { background-color: #007bff; color: white !important; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<h2>${userType} Password Reset Request</h2>
<p>You requested to reset your password. Click the button below to create a new password:</p>
<p><a href="${resetLink}" class="button">Reset Password</a></p>
<p>This link will expire in 1 hour.</p>
<p>If you didn't request this reset, please ignore this email.</p>
<div class="footer">
<p>This is an automated message, please do not reply to this email.</p>
</div>
</div>
</body>
</html>
`;
}
generatePasswordChangedEmailTemplate(userType) {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.alert { background-color: #d4edda; color: #155724; padding: 12px; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<div class="alert">
<h2>${userType} Password Changed Successfully</h2>
</div>
<p>Your password has been changed successfully.</p>
<p>If you did not make this change, please contact our support team immediately at
<a href="mailto:${process.env.SUPPORT_EMAIL}">${process.env.SUPPORT_EMAIL}</a>.</p>
<p><a href="${process.env.FRONTEND_URL}/login">Login to your account</a></p>
</div>
</body>
</html>
`;
}
}
export default PasswordResetService;

View File

@ -0,0 +1,19 @@
<% const layout = 'layouts/main' %>
<% const title = 'Password Reset Failed' %>
<link href="/public/css/forget-password.css" rel="stylesheet">
<div class="page-center">
<div class="auth-card text-center">
<div class="auth-header">
<div class="auth-icon" style="color:#ef4444;">
<i class="fas fa-exclamation-circle"></i>
</div>
<div class="auth-title">Password Reset Link Expired</div>
<div class="auth-sub"><%= error %></div>
</div>
<div class="mt-4">
<a href="<%= process.env.FRONTEND_URL %>/forgot-password" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
<a href="<%= process.env.FRONTEND_URL %>/login" class="btn btn-ghost">Back to Login</a>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
<% const layout = 'layouts/main' %>
<% const title = 'Reset Password' %>
<link href="/public/css/forget-password.css" rel="stylesheet">
<div class="page-center">
<div class="auth-card" role="main">
<div class="auth-header">
<div class="auth-icon" style="color:#2563eb;">
<i class="fas fa-lock"></i>
</div>
<div class="auth-title">Reset Your Password</div>
<div class="auth-sub">Enter your new password below</div>
</div>
<% if (error) { %>
<div class="alert alert-error"><strong>Error:</strong> <%= error %></div>
<% } %>
<form action="/api/v1/password/reset" method="POST" id="resetForm">
<input type="hidden" name="token" value="<%= token %>">
<input type="hidden" name="userType" value="<%= userType %>">
<div class="form-row">
<input type="password" id="password" name="password" required minlength="6"
class="form-input" placeholder="New Password">
</div>
<div class="form-row">
<input type="password" id="confirmPassword" name="confirmPassword" required minlength="6"
class="form-input" placeholder="Confirm Password">
</div>
<div class="form-row">
<button type="submit" class="btn btn-primary">Reset Password</button>
</div>
</form>
<a href="<%= process.env.FRONTEND_URL %>/login" class="small-link">Back to Login</a>
</div>
</div>
<script>
document.getElementById('resetForm').addEventListener('submit', function(e){
const p = document.getElementById('password').value;
const c = document.getElementById('confirmPassword').value;
if (p.length < 6) {
e.preventDefault();
alert('Password must be at least 6 characters long');
return;
}
if (p !== c) {
e.preventDefault();
alert('Passwords do not match');
return;
}
});
</script>

View File

@ -0,0 +1,18 @@
<% const layout = 'layouts/main' %>
<% const title = 'Password Reset Success' %>
<link href="/public/css/forget-password.css" rel="stylesheet">
<div class="page-center">
<div class="auth-card text-center">
<div class="auth-header">
<div class="auth-icon" style="color:#10b981;">
<i class="fas fa-check-circle"></i>
</div>
<div class="auth-title">Password Reset Successful!</div>
<div class="auth-sub">Your password has been reset. You can now login.</div>
</div>
<div class="mt-4">
<a href="<%= process.env.FRONTEND_URL %>/login" class="btn btn-primary">Go to Login</a>
</div>
</div>
</div>

17
views/layouts/main.ejs Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title><%= title %></title>
<!-- Font Awesome (if you use icons) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom auth css -->
<link href="/public/css/forget-password.css" rel="stylesheet">
</head>
<body>
<%- body %>
</body>
</html>

View File

@ -178,14 +178,10 @@
<input type="password" id="password" class="input-field" placeholder=" " required> <input type="password" id="password" class="input-field" placeholder=" " required>
<label for="password" class="input-label">Create Password *</label> <label for="password" class="input-label">Create Password *</label>
</div> </div>
<div class="password-strength">
<div class="password-strength-bar" id="password-strength-bar"></div>
</div>
<div class="password-actions"> <div class="password-actions">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button> <button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button> <button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
</div> </div>
<div class="form-text">Use at least 8 characters with a mix of letters, numbers, and symbols</div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -291,8 +287,7 @@
<label for="phone" class="input-label">Phone Number *</label> <label for="phone" class="input-label">Phone Number *</label>
<div class="input-container"> <div class="input-container">
<input type="tel" id="phone" class="input-field" placeholder=" " required> <input type="tel" id="phone" class="input-field" placeholder=" " required>
<label for="phone" class="input-label" style="left: 40px;">Phone Number</label> <label for="phone" class="input-label">+ Phone Number</label>
<span style="position: absolute; left: 12px; top: 50%; transform: translateY(-50%);">+</span>
</div> </div>
</div> </div>
@ -315,7 +310,6 @@
<h2 class="form-title">Payment Details</h2> <h2 class="form-title">Payment Details</h2>
<form id="payment-form"> <form id="payment-form">
<div class="form-group"> <div class="form-group">
<label class="input-label">Payment Method *</label>
<div class="payment-option" onclick="selectPaymentMethod('payoneer')"> <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4> <h4>Payoneer</h4>
<p>Receive payments via Payoneer</p> <p>Receive payments via Payoneer</p>