diff --git a/public/css/va-register.css b/public/css/va-register.css index 659500e..3acf759 100644 --- a/public/css/va-register.css +++ b/public/css/va-register.css @@ -475,4 +475,43 @@ body { font-size: 40px; margin-bottom: 40px; } - } \ No newline at end of file + } + + .step-error-container { + background-color: #fef2f2; + border: 1px solid #fecaca; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 20px; + animation: slideDown 0.3s ease-out; +} + +.error-message { + color: #dc2626; + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} + +.error-message::before { + content: "⚠"; + font-size: 16px; +} + +.field-error { + border: 1px solid #dc2626 !important; + background-color: #fef2f2; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index c44cba4..44debf4 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -1,6 +1,6 @@ // Email validation function function validateEmail(email) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/; return emailRegex.test(email); } @@ -35,12 +35,27 @@ function validateCardNumber(cardNumber) { return false; } + // Check for invalid patterns + if (isInvalidCardPattern(cleanNumber)) { + return false; + } + + // Check for invalid BIN ranges + if (hasInvalidBIN(cleanNumber)) { + return false; + } + // Luhn algorithm + return isValidLuhn(cleanNumber); +} + +// Enhanced Luhn algorithm +function isValidLuhn(cardNumber) { let sum = 0; let isEven = false; - for (let i = cleanNumber.length - 1; i >= 0; i--) { - let digit = parseInt(cleanNumber.charAt(i)); + for (let i = cardNumber.length - 1; i >= 0; i--) { + let digit = parseInt(cardNumber.charAt(i)); if (isEven) { digit *= 2; @@ -56,6 +71,66 @@ function validateCardNumber(cardNumber) { return sum % 10 === 0; } +// Function to check for invalid card patterns +function isInvalidCardPattern(cardNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^0123/, // Sequential + /^1234/, // Sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(cardNumber)); +} + +// Function to check for invalid BIN ranges +function hasInvalidBIN(cardNumber) { + const firstSix = cardNumber.substring(0, 6); + const firstFour = cardNumber.substring(0, 4); + const firstTwo = cardNumber.substring(0, 2); + + // Invalid BIN ranges + const invalidBINs = [ + '000000', '000001', '000002', '000003', // Test cards should be handled separately + '111111', '222222', '333333', '444444', + '555555', '666666', '777777', '888888', + '999999', '123456', '654321' + ]; + + // Check specific invalid ranges + if (invalidBINs.includes(firstSix)) { + return true; + } + + // Check for known invalid starting digits + if (['0', '3', '6', '7', '8'].includes(firstTwo)) { + // 0 - Reserved + // 3 - Should only be 34, 37 (AMEX) + // 6 - Should follow Discover patterns + // 7, 8 - Not currently assigned to major networks + if (!isValidCardPrefix(cardNumber)) { + return true; + } + } + + return false; +} + +// Validate card prefix +function isValidCardPrefix(cardNumber) { + const type = detectCardType(cardNumber); + return type !== 'unknown'; +} + + // Expiry date validation function validateExpiryDate(expiryDate) { const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/; @@ -108,11 +183,41 @@ function validateRoutingNumber(routingNumber) { return false; } - // Simple checksum validation - const digits = cleanRouting.split('').map(Number); + // Check for invalid patterns + if (isInvalidRoutingNumberPattern(cleanRouting)) { + return false; + } + + // ABA routing number checksum validation + return isValidABAChecksum(cleanRouting); +} + +function isInvalidRoutingNumberPattern(routingNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^012345678/, // Sequential + /^876543210/, // Reverse sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(routingNumber)); +} + +function isValidABAChecksum(routingNumber) { + const digits = routingNumber.split('').map(Number); + + // ABA routing number checksum formula const sum = (3 * (digits[0] + digits[3] + digits[6])) + (7 * (digits[1] + digits[4] + digits[7])) + - (digits[2] + digits[5] + digits[8]); + (1 * (digits[2] + digits[5] + digits[8])); return sum % 10 === 0; } @@ -235,10 +340,14 @@ function getMaxCompletedStep() { return maxStep; } +async function handleNextStep(step) { + await nextStep(step); +} + // Function to navigate to next step -function nextStep(step) { +async function nextStep(step) { // Validate current step before proceeding - if (!validateStep(currentStep)) { + if (!(await validateStep(currentStep))) { return; } @@ -288,7 +397,7 @@ function prevStep(step) { } // Function to validate current step -function validateStep(step) { +async function validateStep(step) { let isValid = true; const form = document.getElementById(getFormId(step)); @@ -297,7 +406,7 @@ function validateStep(step) { const selectedPayment = document.querySelector('.payment-option.selected'); if (!selectedPayment) { isValid = false; - alert('Please select a payment method'); + showError('Please select a payment method', null, step); return isValid; } @@ -311,10 +420,9 @@ function validateStep(step) { const cardNumber = document.getElementById('card-number').value; if (!validateCardNumber(cardNumber)) { isValid = false; - document.getElementById('card-number').style.border = '1px solid #ef4444'; - alert('Please enter a valid card number'); + showError('Please enter a valid card number', 'card-number', step); + return isValid; } else { - document.getElementById('card-number').style.border = ''; // Show card type const cardType = detectCardType(cardNumber); if (cardType !== 'unknown') { @@ -327,10 +435,8 @@ function validateStep(step) { const expiryDate = document.getElementById('expiry-date').value; if (!validateExpiryDate(expiryDate)) { isValid = false; - document.getElementById('expiry-date').style.border = '1px solid #ef4444'; - alert('Please enter a valid expiry date (MM/YY)'); - } else { - document.getElementById('expiry-date').style.border = ''; + showError('Please enter a valid expiry date (MM/YY)', 'expiry-date', step); + return isValid; } // CVV validation @@ -338,20 +444,16 @@ function validateStep(step) { const cardType = detectCardType(cardNumber); if (!validateCVV(cvv, cardType)) { isValid = false; - document.getElementById('security-code').style.border = '1px solid #ef4444'; - alert(`Please enter a valid ${cardType === 'amex' ? '4-digit' : '3-digit'} CVV`); - } else { - document.getElementById('security-code').style.border = ''; + showError(`Please enter a valid ${cardType === 'amex' ? '4-digit' : '3-digit'} CVV`, 'security-code', step); + return isValid; } // ZIP code validation const zipCode = document.getElementById('card-zip').value; if (!validateZipCode(zipCode)) { isValid = false; - document.getElementById('card-zip').style.border = '1px solid #ef4444'; - alert('Please enter a valid ZIP code'); - } else { - document.getElementById('card-zip').style.border = ''; + showError('Please enter a valid ZIP code', 'card-zip', step); + return isValid; } } @@ -361,41 +463,54 @@ function validateStep(step) { const accountNumber = document.getElementById('account-number').value; if (!validateBankAccount(accountNumber)) { isValid = false; - document.getElementById('account-number').style.border = '1px solid #ef4444'; - alert('Please enter a valid account number (8-17 digits)'); - } else { - document.getElementById('account-number').style.border = ''; + showError('Please enter a valid account number (8-17 digits)', 'account-number', step); + return isValid; } // Routing number validation const routingNumber = document.getElementById('routing-number').value; if (!validateRoutingNumber(routingNumber)) { isValid = false; - document.getElementById('routing-number').style.border = '1px solid #ef4444'; - alert('Please enter a valid routing number'); - } else { - document.getElementById('routing-number').style.border = ''; + showError('Please enter a valid routing numbers', 'routing-number', step); + return isValid; } // Basic validation for other bank fields requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); + if (isValid === false) { + showError(`Please fill in ${field.labels[0]?.textContent || 'this field'}`, field.id, step); + } } else { - field.style.border = ''; + field.classList.remove('field-error'); } }); } } } else if (step === 1) { const email = document.getElementById('email').value; + const companyName = document.getElementById('company-name').value; + if (!validateEmail(email) && !companyName) { + isValid = false; + showError('Both Fields are Required', null, step); + return isValid; + } if (!validateEmail(email)) { isValid = false; - document.getElementById('email').style.border = '1px solid #ef4444'; - alert('Please enter a valid email address'); + showError('Please enter a valid email address', 'email', step); } else { - document.getElementById('email').style.border = ''; + const isEmailVerified = await verifyEmail(email); + if (!isEmailVerified) { + showError('Email already registered. Please use a different email address.', 'email', step); + isValid = false; + } + } + if (!companyName) { + isValid = false; + showError('Company / Client Name is Required', null, step); + return isValid; } // Validate other required fields @@ -403,19 +518,16 @@ function validateStep(step) { requiredFields.forEach(field => { if (field.id !== 'email' && !field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else if (field.id !== 'email') { - field.style.border = ''; + field.classList.remove('field-error'); } }); } else if (step === 3) { const email = document.getElementById('billing-email').value; if (!validateEmail(email)) { isValid = false; - document.getElementById('billing-email').style.border = '1px solid #ef4444'; - alert('Please enter a valid email address'); - } else { - document.getElementById('billing-email').style.border = ''; + showError('Please enter a valid email address', 'billing-email', step); } // Validate other required fields @@ -423,19 +535,27 @@ function validateStep(step) { requiredFields.forEach(field => { if (field.id !== 'email' && !field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else if (field.id !== 'email') { - field.style.border = ''; + field.classList.remove('field-error'); } }); } else if (step === 4) { const email = document.getElementById('signatory-email').value; if (!validateEmail(email)) { isValid = false; - document.getElementById('signatory-email').style.border = '1px solid #ef4444'; - alert('Please enter a valid email address'); - } else { - document.getElementById('signatory-email').style.border = ''; + showError('Please enter a valid email address', 'signatory-email', step); + } + const signatoryName = document.getElementById('signatory-name').value; + if (!signatoryName) { + isValid = false; + showError('Please enter a valid name', 'signatory-name', step); + } + + if (!signatoryName && !validateEmail(email)) { + isValid = false; + showError('Name and Email are Required', 'signatory-name', step); + return isValid; } // Validate other required fields @@ -443,9 +563,9 @@ function validateStep(step) { requiredFields.forEach(field => { if (field.id !== 'email' && !field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else if (field.id !== 'email') { - field.style.border = ''; + field.classList.remove('field-error'); } }); } else { @@ -454,9 +574,9 @@ function validateStep(step) { requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else { - field.style.border = ''; + field.classList.remove('field-error'); } }); } @@ -553,7 +673,7 @@ function selectPaymentOption(type) { // Function to submit the form async function submitForm() { // Validate current step - if (!validateStep(currentStep)) { + if (!(await validateStep(currentStep))) { return; } @@ -610,15 +730,15 @@ async function submitForm() { } else { // Handle server errors if (response.status === 409) { - alert('A user with this email already exists. Please use a different email.'); + showError('A user with this email already exists. Please use a different email.', null, 5); } else { - alert('There was an error submitting your registration. Please try again.'); + showError('There was an error submitting your registration. Please try again.', null, 5); } } } catch (error) { // Handle network errors console.error('Error:', error); - alert('There was a network error. Please check your connection and try again.'); + showError('There was a network error. Please check your connection and try again.', null, 5); } finally { // Restore button state submitBtn.textContent = originalText; @@ -682,14 +802,14 @@ async function detectLocation() { ? data.country_code : 'US'; const region = data.region || ''; - const city = data.city || ''; - const zip = data.postal || ''; + // const city = data.city || ''; + // const zip = data.postal || ''; // Populate city and state based on detected location - const cityInput = document.getElementById('city'); - const zipInput = document.getElementById('zip'); - if (cityInput) cityInput.value = city; - if (zipInput) zipInput.value = zip; + // const cityInput = document.getElementById('city'); + // const zipInput = document.getElementById('zip'); + // if (cityInput) cityInput.value = city; + // if (zipInput) zipInput.value = zip; // Populate states dropdown for the detected country await populateStates(countryCode, 'state'); @@ -863,6 +983,20 @@ document.addEventListener('DOMContentLoaded', function() { input.dispatchEvent(new Event('input')); }); + // Trigger change event for select + document.getElementById('billing-state').dispatchEvent(new Event('change')); + } else { + document.getElementById('billing-name').value = ''; + document.getElementById('billing-address').value = ''; + document.getElementById('billing-city').value = ''; + document.getElementById('billing-state').value = ''; + document.getElementById('billing-zip').value = ''; + + // Trigger input events to update labels + document.querySelectorAll('#billing-form .input-field').forEach(input => { + input.dispatchEvent(new Event('input')); + }); + // Trigger change event for select document.getElementById('billing-state').dispatchEvent(new Event('change')); } @@ -873,8 +1007,856 @@ document.addEventListener('DOMContentLoaded', function() { if (this.checked) { document.getElementById('billing-email').value = formData.userDetails.email || ''; document.getElementById('billing-email').dispatchEvent(new Event('input')); + } else { + document.getElementById('billing-email').value = ''; + document.getElementById('billing-email').dispatchEvent(new Event('input')); } }); // Detect user location when the page loads detectLocation(); -}); \ No newline at end of file +}); + +async function verifyEmail(email) { + + try { + const response = await fetch('/api/v1/client/verify-client-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({"email": email}) + }); + + if (response.ok) { + return false; + } else { + return true; + } + } catch (error) { + // Handle network errors + console.error('Error:', error); + showError('There was a network error. Please check your connection and try again.', null, 1); + return false; + } +} + +function showError(message, fieldId = null, step = null) { + // Determine the current step if not provided + const currentStep = step || getCurrentStep(); + const errorContainer = document.getElementById(`step-${currentStep}-error`); + const errorMessage = document.getElementById(`step-${currentStep}-error-message`); + + if (errorContainer && errorMessage) { + errorMessage.textContent = message; + errorContainer.style.display = 'block'; + + // Scroll to error container + errorContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // Highlight specific field if provided + if (fieldId) { + const field = document.getElementById(fieldId); + if (field) { + field.classList.add('field-error'); + // Remove error after 5 seconds + setTimeout(() => { + field.classList.remove('field-error'); + }, 5000); + } + } + + // Auto-hide error after 8 seconds + setTimeout(() => { + hideError(currentStep); + }, 8000); + } +} + +function hideError(step = null) { + if (step) { + // Hide specific step error + const errorContainer = document.getElementById(`step-${step}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } else { + // Hide all step errors + for (let i = 1; i <= 5; i++) { + const errorContainer = document.getElementById(`step-${i}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } + } +} + +function clearFieldErrors() { + document.querySelectorAll('.field-error').forEach(field => { + field.classList.remove('field-error'); + }); +} + +function getCurrentStep() { + return currentStep; +} + +// Function to validate signatory name (no numbers allowed) +function validateSignatoryName(input) { + const errorElement = document.getElementById('signatory-name-error'); + const value = input.value; + + // Remove any numbers from the input + const cleanValue = value.replace(/[0-9]/g, ''); + + // If numbers were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Show error if original value contained numbers + if (value !== cleanValue && value.length > 0) { + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent number input in real-time +function preventNumbers(event) { + const charCode = event.which ? event.which : event.keyCode; + + // Allow letters, spaces, hyphens, apostrophes, and backspace + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 8 || // backspace + charCode === 46) { // delete + return true; + } + + // Prevent numbers and other characters + event.preventDefault(); + return false; +} + +// Function to validate company/client name +function validateCompanyName(input) { + const errorElement = document.getElementById('company-name-error'); + const value = input.value; + + // Check various invalid patterns + const validationResult = validateCompanyNameContent(value); + + if (!validationResult.isValid && value.trim().length > 0) { + errorElement.textContent = validationResult.message; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function validateCompanyNameContent(companyName) { + const trimmed = companyName.trim(); + + if (trimmed.length === 0) { + return { isValid: false, message: 'Company name is required' }; + } + + // Check minimum length + if (trimmed.length < 2) { + return { isValid: false, message: 'Company name must be at least 2 characters' }; + } + + // Remove whitespace for content analysis + const content = trimmed.replace(/\s/g, ''); + + // Check if contains only special characters + if (/^[^a-zA-Z0-9]+$/.test(content)) { + return { isValid: false, message: 'Company name cannot be only special characters' }; + } + + // Check if contains at least some valid content (letters or numbers) + if (!/[a-zA-Z0-9]/.test(content)) { + return { isValid: false, message: 'Company name must contain letters or numbers' }; + } + + // Optional: Check for excessive special characters (more than 50%) + const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length; + if (specialCharCount > content.length / 2) { + return { isValid: false, message: 'Company name contains too many special characters' }; + } + + return { isValid: true, message: '' }; +} + +// Function to prevent invalid character input in real-time +function preventInvalidCompanyChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas + // Also allow common editing keys + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + (charCode >= 48 && charCode <= 57) || // 0-9 + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 38 || // ampersand (&) + charCode === 46 || // period (.) + charCode === 44 || // comma (,) + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91 || // cmd (Mac) + /^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters + return true; + } + + // Prevent invalid characters + event.preventDefault(); + return false; +} + +// Function to validate ZIP code in real-time +function validateZipCode(input) { + let errorElement; + + // Determine which error element to use based on input ID + if (input.id === 'billing-zip') { + errorElement = document.getElementById('billing-zip-error'); + } else if (input.id === 'card-zip') { + errorElement = document.getElementById('card-zip-error'); + } else { + errorElement = document.getElementById('zip-error'); + } + + const value = input.value; + + // Remove any non-numeric characters except hyphens + const cleanValue = value.replace(/[^0-9\-]/g, ''); + + // If invalid characters were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Format ZIP code as user types (add hyphen after 5 digits) + if (cleanValue.length === 5 && !cleanValue.includes('-')) { + input.value = cleanValue; + } else if (cleanValue.length > 5 && !cleanValue.includes('-')) { + input.value = cleanValue.substring(0, 5) + '' + cleanValue.substring(5, 9); + } + + + if (input.value.length < 4) { + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent non-numeric input in ZIP fields +function preventNonNumericZip(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + const currentValue = event.target.value; + + // Allow: numbers, hyphens (only in specific positions), and editing keys + if ((charCode >= 48 && charCode <= 57) || // 0-9 + (charCode === 45 && currentValue.length === 5 && !currentValue.includes('-')) || // hyphen only after 5 digits + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91) { // cmd (Mac) + return true; + } + + // Prevent all other characters including letters and special characters + event.preventDefault(); + return false; +} + +// Function to validate city name in real-time +function validateCity(input) { + let errorElement; + + // Determine which error element to use based on input ID + if (input.id === 'billing-city') { + errorElement = document.getElementById('billing-city-error'); + } else { + errorElement = document.getElementById('city-error'); + } + + const value = input.value; + + // Remove invalid special characters (keep only letters, spaces, hyphens, apostrophes) + const cleanValue = value.replace(/[^a-zA-Z\s\-']/g, ''); + + // If invalid characters were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check if the value contains only numbers + const numbersOnlyCheck = value.replace(/[\s\-']/g, ''); + const hasOnlyNumbers = /^[0-9]+$/.test(numbersOnlyCheck) && numbersOnlyCheck.length > 0; + + // Check if the value contains only special characters + const specialCharsOnlyCheck = value.replace(/[\s]/g, ''); + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(specialCharsOnlyCheck) && specialCharsOnlyCheck.length > 0; + + // Check if we have at least one letter + const hasLetters = /[a-zA-Z]/.test(value); + + // Show appropriate error messages + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'City name can only contain letters, spaces, hyphens, and apostrophes'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'City name cannot be only numbers. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'City name cannot be only special characters. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'City name must include at least one letter'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent invalid character input in city fields +function preventInvalidCityChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow: letters, spaces, hyphens, apostrophes, and editing keys + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91 || // cmd (Mac) + /^[a-zA-Z\s\-']$/.test(key)) { // allowed characters + return true; + } + + // Prevent numbers and other special characters + event.preventDefault(); + return false; +} + +function validatePosition(input) { + const errorElement = document.getElementById('position-error'); + const value = input.value; + + // Remove numbers and invalid special characters + // Allow: letters, spaces, hyphens, apostrophes, periods, commas, ampersands, slashes + const cleanValue = value.replace(/[^a-zA-Z\s\-'.,&\/]/g, ''); + + // If invalid characters were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check if the value contains only numbers + const numbersOnlyCheck = value.replace(/[\s\-'.,&\/]/g, ''); + const hasOnlyNumbers = /^[0-9]+$/.test(numbersOnlyCheck) && numbersOnlyCheck.length > 0; + + // Check if the value contains only special characters + const specialCharsOnlyCheck = value.replace(/[\s]/g, ''); + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(specialCharsOnlyCheck) && specialCharsOnlyCheck.length > 0; + + // Check if we have at least one letter + const hasLetters = /[a-zA-Z]/.test(value); + + // Show appropriate error messages + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Position can only contain letters, spaces, hyphens, apostrophes, periods, commas, ampersands, and slashes'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Position cannot be only numbers. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Position cannot be only special characters. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Position must include at least one letter'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent invalid character input in position field +function preventInvalidPositionChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow: letters, spaces, hyphens, apostrophes, periods, commas, ampersands, slashes + // Also allow common editing keys + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 46 || // period + charCode === 44 || // comma + charCode === 38 || // ampersand (&) + charCode === 47 || // slash (/) + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91 || // cmd (Mac) + /^[a-zA-Z\s\-'.,&\/]$/.test(key)) { // allowed characters + return true; + } + + // Prevent numbers and other special characters + event.preventDefault(); + return false; +} + +// Function to validate CVV in real-time +function validateCVVVV(input) { + const errorElement = document.getElementById('security-code-error'); + const value = input.value; + + // Remove any non-digit characters + const cleanValue = value.replace(/\D/g, ''); + + // If non-digits were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Get card type to determine expected CVV length + const cardNumber = document.getElementById('card-number').value; + const cardType = detectCardType(cardNumber); + + // Validate CVV format and content + const isValid = validateCVVStrict(cleanValue, cardType); + + if (!isValid && cleanValue.length > 0) { + errorElement.textContent = getCVVErrorMessage(cleanValue, cardType); + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent invalid character input in CVV field +function preventInvalidCVVChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow only digits and common editing keys + if ((charCode >= 48 && charCode <= 57) || // 0-9 + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91) { // cmd (Mac) + return true; + } + + // Prevent all non-digit characters + event.preventDefault(); + return false; +} + +// Enhanced CVV validation function +function validateCVVStrict(cvv, cardType) { + if (!cvv || !/^\d+$/.test(cvv)) { + return false; + } + + // Check for invalid patterns + if (isInvalidCVVPattern(cvv)) { + return false; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + + // Check length + if (cvv.length !== expectedLength) { + return false; + } + + return true; +} + +// Function to check for invalid CVV patterns +function isInvalidCVVPattern(cvv) { + const invalidPatterns = [ + /^0+$/, // All zeros: 000, 0000 + /^1+$/, // All ones: 111, 1111 + /^2+$/, // All twos: 222, 2222 + /^3+$/, // All threes: 333, 3333 + /^4+$/, // All fours: 444, 4444 + /^5+$/, // All fives: 555, 5555 + /^6+$/, // All sixes: 666, 6666 + /^7+$/, // All sevens: 777, 7777 + /^8+$/, // All eights: 888, 8888 + /^9+$/, // All nines: 999, 9999 + ]; + + return invalidPatterns.some(pattern => pattern.test(cvv)); +} + +// Function to get appropriate CVV error message +function getCVVErrorMessage(cvv, cardType) { + if (!cvv) { + return 'Security code is required'; + } + + if (!/^\d+$/.test(cvv)) { + return 'Security code must contain only numbers'; + } + + if (isInvalidCVVPattern(cvv)) { + return 'Please enter a valid security code'; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + + if (cvv.length !== expectedLength) { + if (cardType === 'amex') { + return 'American Express cards require a 4-digit security code'; + } else { + return `Security code must be ${expectedLength} digits`; + } + } + + return 'Please enter a valid security code'; +} + +// Bank Name validation +function validateBankName(input) { + const errorElement = document.getElementById('bank-name-error'); + const value = input.value; + + // Remove numbers and invalid special characters + const cleanValue = value.replace(/[^a-zA-Z\s\-'&.,()]/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check content + const contentCheck = value.replace(/[\s\-'&.,()]/g, ''); + const hasOnlyNumbers = /^[0-9]+$/.test(contentCheck) && contentCheck.length > 0; + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(contentCheck) && contentCheck.length > 0; + const hasLetters = /[a-zA-Z]/.test(value); + + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Bank name can only contain letters, spaces, and common punctuation'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Bank name cannot be only numbers'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Bank name cannot be only special characters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Bank name must include letters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidBankNameChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + if ((charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || + charCode === 32 || charCode === 45 || charCode === 39 || + charCode === 38 || charCode === 46 || charCode === 44 || + charCode === 40 || charCode === 41 || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91 || + /^[a-zA-Z\s\-'&.,()]$/.test(key)) { + return true; + } + + event.preventDefault(); + return false; +} + +// Enhanced Account Number validation +function validateAccountNumber(input) { + const errorElement = document.getElementById('account-number-error'); + const value = input.value.replace(/\s/g, ''); + + // Remove any non-digit characters + const cleanValue = value.replace(/\D/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Validate account number + const isValid = validateAccountNumberStrict(cleanValue); + + if (!isValid && cleanValue.length > 0) { + errorElement.textContent = getAccountNumberErrorMessage(cleanValue); + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidAccountNumberChars(event) { + const charCode = event.which ? event.which : event.keyCode; + + if ((charCode >= 48 && charCode <= 57) || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91) { + return true; + } + + event.preventDefault(); + return false; +} + +function validateAccountNumberStrict(accountNumber) { + if (!accountNumber || !/^\d+$/.test(accountNumber)) { + return false; + } + + // Check length + if (accountNumber.length < 8 || accountNumber.length > 17) { + return false; + } + + // Check for invalid patterns + if (isInvalidAccountNumberPattern(accountNumber)) { + return false; + } + + // Simple checksum validation (basic example) + if (!isValidAccountNumberChecksum(accountNumber)) { + return false; + } + + return true; +} + +function isInvalidAccountNumberPattern(accountNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^12345678/, // Sequential + /^87654321/, // Reverse sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(accountNumber)); +} + +function isValidAccountNumberChecksum(accountNumber) { + // Basic checksum validation - you might want to implement + // a more specific algorithm based on bank requirements + let sum = 0; + for (let i = 0; i < accountNumber.length; i++) { + sum += parseInt(accountNumber.charAt(i)); + } + + // Simple check: sum shouldn't be too low (e.g., all zeros) + return sum > 0; +} + +function getAccountNumberErrorMessage(accountNumber) { + if (!accountNumber) { + return 'Account number is required'; + } + + if (!/^\d+$/.test(accountNumber)) { + return 'Account number must contain only numbers'; + } + + if (accountNumber.length < 8) { + return 'Account number must be at least 8 digits'; + } + + if (accountNumber.length > 17) { + return 'Account number cannot exceed 17 digits'; + } + + if (isInvalidAccountNumberPattern(accountNumber)) { + return 'Please enter a valid account number'; + } + + return 'Please enter a valid account number'; +} + +// Account Holder Name validation +function validateAccountHolderName(input) { + const errorElement = document.getElementById('account-holder-name-error'); + const value = input.value; + + // Remove numbers and special characters (keep only letters and spaces) + const cleanValue = value.replace(/[^a-zA-Z\s]/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check content + const hasOnlyNumbers = /^[0-9]+$/.test(value.replace(/\s/g, '')); + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(value.replace(/\s/g, '')); + const hasLetters = /[a-zA-Z]/.test(value); + + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Account holder name can only contain letters and spaces'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Account holder name cannot contain only numbers'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Account holder name cannot contain only special characters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Account holder name must include letters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidAccountHolderChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + if ((charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || + charCode === 32 || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91 || + /^[a-zA-Z\s]$/.test(key)) { + return true; + } + + event.preventDefault(); + return false; +} \ No newline at end of file diff --git a/public/js/va-register.js b/public/js/va-register.js index 2c65a26..8b2731c 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -109,7 +109,22 @@ // Add input event listeners to handle label animation - document.querySelectorAll('.input-field').forEach(input => { + document.querySelectorAll('.input-field:not(.dob)').forEach(input => { + setTimeout(() => { + if (input.value) { + input.nextElementSibling.style.top = '4px'; + input.nextElementSibling.style.fontSize = '12px'; + input.nextElementSibling.style.transform = 'none'; + } + }, 100); + + input.addEventListener('animationstart', function() { + if (this.value) { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + } + }); input.addEventListener('input', function() { if (this.value) { this.nextElementSibling.style.top = '4px'; @@ -139,6 +154,13 @@ // Handle select field label animation document.querySelectorAll('.select-field').forEach(select => { + setTimeout(() => { + if (select.value) { + select.nextElementSibling.style.top = '4px'; + select.nextElementSibling.style.fontSize = '12px'; + select.nextElementSibling.style.transform = 'none'; + } + }, 100); select.addEventListener('change', function() { if (this.value) { this.nextElementSibling.style.top = '4px'; @@ -228,10 +250,14 @@ return maxStep; } + async function handleNextStep(step) { + await nextStep(step); + } + // Function to navigate to next step - function nextStep(step) { + async function nextStep(step) { // Validate current step before proceeding - if (!validateStep(currentStep)) { + if (!(await validateStep(currentStep))) { return; } @@ -306,9 +332,13 @@ } // Function to validate current step - function validateStep(step) { + async function validateStep(step) { let isValid = true; const form = document.getElementById(getFormId(step)); + + // Hide any previous errors for this step + hideError(step); + clearFieldErrors(); if (form) { // For account setup step @@ -319,21 +349,31 @@ // Email validation if (!validateEmail(email)) { - alert('Please enter a valid email address'); + showError('Please enter a valid email address', 'email', step); isValid = false; + } else { + // Email verification - wait for the result + const isEmailVerified = await verifyEmail(email); + if (!isEmailVerified) { + showError('Email already registered. Please use a different email address.', 'email', step); + isValid = false; + } } - + // Password validation if (password.length < 8) { - alert('Password must be at least 8 characters long'); + 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'); } } @@ -342,15 +382,18 @@ const firstName = document.getElementById('firstName').value; if (!firstName) { document.getElementById('profile-incomplete').style.display = 'block'; + document.getElementById('firstName').classList.add('field-error'); + showError('First name is required', 'firstName', step); isValid = false; } else { document.getElementById('profile-incomplete').style.display = 'none'; + document.getElementById('firstName').classList.remove('field-error'); } // Age validation const age = document.getElementById('age').value; if (age && (age < 18 || age > 100)) { - alert('You must be 18 years or older to register'); + showError('You must be 18 years or older to register', 'dob', step); isValid = false; } @@ -359,12 +402,12 @@ const state = document.getElementById('state').value; if (!country) { - alert('Please select your country'); + showError('Please select your country', 'country', step); isValid = false; } if (!state) { - alert('Please select your state/region'); + showError('Please select your state/region', 'state', step); isValid = false; } @@ -373,9 +416,40 @@ requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); + if (isValid === false) { + showError(`Please fill in ${field.labels[0]?.textContent || 'this field'}`, field.id, step); + } } else { - field.style.border = ''; + field.classList.remove('field-error'); + } + }); + } + + if (step === 3) { + // Phone number validation + const phone = document.getElementById('phone').value; + if (!phone) { + showError('Please enter your phone number', 'phone', step); + isValid = false; + } else if (!validatePhoneNumberStrict(phone)) { + document.getElementById('phone-error').style.display = 'block'; + document.getElementById('phone').classList.add('field-error'); + showError('Please enter a valid phone number (10-15 digits)', 'phone', step); + isValid = false; + } else { + document.getElementById('phone-error').style.display = 'none'; + document.getElementById('phone').classList.remove('field-error'); + } + + // Validate other required fields in step 3 + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.classList.add('field-error'); + } else { + field.classList.remove('field-error'); } }); } @@ -384,20 +458,20 @@ if (step === 4) { const selectedPayment = document.querySelector('.payment-option.selected'); if (!selectedPayment) { - alert('Please select a payment method'); + showError('Please select a payment method', null, step); isValid = false; } else { const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; if (paymentMethod === 'payoneer') { const payoneerAccount = document.getElementById('payoneerAccount').value; if (!payoneerAccount) { - alert('Please enter your Payoneer account number'); + showError('Please enter your Payoneer account number', 'payoneerAccount', step); isValid = false; } } else if (paymentMethod === 'veem') { const veemAccount = document.getElementById('veemAccount').value; if (!veemAccount) { - alert('Please enter your Veem account number'); + showError('Please enter your Veem account number', 'veemAccount', step); isValid = false; } } @@ -410,9 +484,9 @@ requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else { - field.style.border = ''; + field.classList.remove('field-error'); } }); } @@ -598,12 +672,12 @@ }, 1000); } else { // Handle server errors - alert('There was an error submitting your registration. Please try again.'); + showError('There was an error submitting your registration. Please try again.', null, 4); } } catch (error) { // Handle network errors console.error('Error:', error); - alert('There was a network error. Please check your connection and try again.'); + showError('There was a network error. Please check your connection and try again.', null, 4); } finally { // Restore button state submitBtn.textContent = originalText; @@ -611,3 +685,218 @@ } } + async function verifyEmail(email) { + + try { + const response = await fetch('/api/v1/va/verify-va-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({"email": email}) + }); + + if (response.ok) { + return false; + } else { + return true; + } + } catch (error) { + // Handle network errors + console.error('Error:', error); + showError('There was a network error. Please check your connection and try again.', null, 1); + return false; + } + } + + // Function to calculate age from date of birth + function calculateAge(dob) { + if (!dob) return ''; + + const birthDate = new Date(dob); + const today = new Date(); + + let age = today.getFullYear() - birthDate.getFullYear(); + const monthDiff = today.getMonth() - birthDate.getMonth(); + + // Adjust age if birthday hasn't occurred this year yet + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + + return age; + } + + // Function to handle date of birth change and update age + function handleDOBChange() { + const dob = document.getElementById('dob').value; + const ageInput = document.getElementById('age'); + + if (dob) { + const age = calculateAge(dob); + ageInput.value = age; + + // Trigger input event to update the label position + ageInput.dispatchEvent(new Event('input')); + + // Validate age + if (age < 18) { + alert('You must be 18 years or older to register'); + ageInput.style.border = '1px solid #ef4444'; + } else if (age > 100) { + alert('Please enter a valid age'); + ageInput.style.border = '1px solid #ef4444'; + } else { + ageInput.style.border = ''; + } + } else { + // Clear age if DOB is cleared + ageInput.value = ''; + ageInput.dispatchEvent(new Event('input')); + } + } + + // Function to handle age manual input and update DOB (optional) + function handleAgeChange() { + const age = document.getElementById('age').value; + const dobInput = document.getElementById('dob'); + + if (age && age >= 18 && age <= 100) { + // Calculate approximate DOB based on age + const today = new Date(); + const birthYear = today.getFullYear() - age; + const approximateDOB = `${birthYear}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + + // You can set this as the DOB or just leave it for reference + // dobInput.value = approximateDOB; + // dobInput.dispatchEvent(new Event('change')); + } + } + + // Function to validate phone number in real-time + function validatePhoneNumber(input) { + const phoneError = document.getElementById('phone-error'); + const value = input.value; + + // Remove any non-digit characters + const digitsOnly = value.replace(/\D/g, ''); + + // Update the input value with digits only + if (value !== digitsOnly) { + input.value = digitsOnly; + } + + // Validate length + if (digitsOnly.length > 0 && (digitsOnly.length < 10 || digitsOnly.length > 15)) { + phoneError.style.display = 'block'; + input.style.border = '1px solid #ef4444'; + return false; + } else { + phoneError.style.display = 'none'; + input.style.border = ''; + return true; + } + } + + // Function to validate phone number on form submission + function validatePhoneNumberStrict(phone) { + const phoneRegex = /^[0-9]{10,15}$/; + return phoneRegex.test(phone); + } + + // Function to validate Payoneer account number + function validatePayoneerAccount(input) { + const payoneerError = document.getElementById('payoneer-error'); + const value = input.value.trim(); + + // Payoneer typically uses email format or alphanumeric IDs + // Adjust validation rules based on Payoneer's actual format requirements + if (value.length > 0 && (value.length < 5 || value.length > 20)) { + payoneerError.textContent = 'Payoneer account must be 5-20 characters'; + payoneerError.style.display = 'block'; + input.style.border = '1px solid #ef4445'; + return false; + } else { + payoneerError.style.display = 'none'; + input.style.border = ''; + return true; + } + } + + // Function to validate Veem account number + function validateVeemAccount(input) { + const veemError = document.getElementById('veem-error'); + const value = input.value.trim(); + + // Veem account numbers are typically longer alphanumeric strings + if (value.length > 0 && (value.length < 8 || value.length > 25)) { + veemError.textContent = 'Veem account must be 8-25 characters'; + veemError.style.display = 'block'; + input.style.border = '1px solid #ef4445'; + return false; + } else { + veemError.style.display = 'none'; + input.style.border = ''; + return true; + } + } + + function showError(message, fieldId = null, step = null) { + // Determine the current step if not provided + const currentStep = step || getCurrentStep(); + const errorContainer = document.getElementById(`step-${currentStep}-error`); + const errorMessage = document.getElementById(`step-${currentStep}-error-message`); + + if (errorContainer && errorMessage) { + errorMessage.textContent = message; + errorContainer.style.display = 'block'; + + // Scroll to error container + errorContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // Highlight specific field if provided + if (fieldId) { + const field = document.getElementById(fieldId); + if (field) { + field.classList.add('field-error'); + // Remove error after 5 seconds + setTimeout(() => { + field.classList.remove('field-error'); + }, 5000); + } + } + + // Auto-hide error after 8 seconds + setTimeout(() => { + hideError(currentStep); + }, 8000); + } + } + + function hideError(step = null) { + if (step) { + // Hide specific step error + const errorContainer = document.getElementById(`step-${step}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } else { + // Hide all step errors + for (let i = 1; i <= 5; i++) { + const errorContainer = document.getElementById(`step-${i}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } + } + } + + function clearFieldErrors() { + document.querySelectorAll('.field-error').forEach(field => { + field.classList.remove('field-error'); + }); + } + + function getCurrentStep() { + return currentStep; + } \ No newline at end of file diff --git a/src/controllers/client/vaClientEmailVerify.controller.js b/src/controllers/client/vaClientEmailVerify.controller.js new file mode 100644 index 0000000..5813e41 --- /dev/null +++ b/src/controllers/client/vaClientEmailVerify.controller.js @@ -0,0 +1,32 @@ +import { ClientModel } from "../../models/vaModel.js"; + +// Verify VA Email +async function client_verify_email(req, res) { + try { + // const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body || {}; + const { email } = payload; + + if (!email) { + return res.status(400).json({ success: false, message: "Please provide email" }); + } + + // Find user by email + const user = await ClientModel.findOne({ "userDetails.email": email }); + if (!user) { + return res.status(404).json({ success: false, message: "Email not found" }); + } + + // Return accessToken and user info + return res.status(200).json({ + success: true, + email: user.userDetails?.email, + message: "Email ID is already registerd" + }); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { client_verify_email }; \ No newline at end of file diff --git a/src/controllers/va/vaAuth/vaVerifyEmail.controller.js b/src/controllers/va/vaAuth/vaVerifyEmail.controller.js new file mode 100644 index 0000000..4aa278e --- /dev/null +++ b/src/controllers/va/vaAuth/vaVerifyEmail.controller.js @@ -0,0 +1,32 @@ +import { VAModel } from "../../../models/vaModel.js"; + +// Verify VA Email +async function va_verify_email(req, res) { + try { + // const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body || {}; + const { email } = payload; + + if (!email) { + return res.status(400).json({ success: false, message: "Please provide email" }); + } + + // Find user by email + const user = await VAModel.findOne({ "userDetails.email": email }); + if (!user) { + return res.status(404).json({ success: false, message: "Invalid Email ID" }); + } + + // Return accessToken and user info + return res.status(200).json({ + success: true, + email: user.userDetails?.email, + message: "Email verified successfully" + }); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_verify_email }; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 941632f..79bc669 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -9,13 +9,15 @@ import { client_refresh_token_callback } from "../../controllers/client/clientRe import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js"; import { clientAuthValidator } from "../../middlewares/auth.middleware.js"; import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js"; -import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js" +import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js"; +import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js"; // clients routes clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/login", va_login_client); // Login Client clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token clientRoutes.post("/forgot-password", client_request_password_reset); +clientRoutes.post("/verify-client-email", client_verify_email); // clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 7beaf43..0686e9d 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -22,6 +22,7 @@ import { va_timesheet_summary_function } from "../../controllers/va/vaTimesheetA import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js"; import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js" import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js" +import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.controller.js"; vaRoutes.get("/", va_callback); // VA Callback for getting active VA @@ -30,6 +31,7 @@ vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for l vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token vaRoutes.post("/register", va_register); // Register a new VA vaRoutes.post("/forgot-password", va_request_password_reset); +vaRoutes.post("/verify-va-email", va_verify_email); vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile diff --git a/views/va_client_register.ejs b/views/va_client_register.ejs index b9c7d30..3dc00f9 100644 --- a/views/va_client_register.ejs +++ b/views/va_client_register.ejs @@ -92,12 +92,18 @@