updated contact form logic
This commit is contained in:
152
production-email-test.cjs
Normal file
152
production-email-test.cjs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Production Email Test Script
|
||||||
|
*
|
||||||
|
* This script tests email delivery in production mode by sending test emails
|
||||||
|
* with various configurations to help diagnose delivery issues.
|
||||||
|
*
|
||||||
|
* Run with: NODE_ENV=production node production-email-test.cjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
const {
|
||||||
|
SMTP_HOST = '',
|
||||||
|
SMTP_PORT = '587',
|
||||||
|
SMTP_USER = '',
|
||||||
|
SMTP_PASS = '',
|
||||||
|
ADMIN_EMAIL = 'richard@bergsma.it',
|
||||||
|
WEBSITE_NAME = 'bergsma.it',
|
||||||
|
NODE_ENV = 'production'
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
// Force production mode
|
||||||
|
const isProduction = true;
|
||||||
|
|
||||||
|
console.log('Production Email Test');
|
||||||
|
console.log('--------------------');
|
||||||
|
console.log('Configuration:');
|
||||||
|
console.log(`SMTP Host: ${SMTP_HOST}`);
|
||||||
|
console.log(`SMTP Port: ${SMTP_PORT}`);
|
||||||
|
console.log(`SMTP User: ${SMTP_USER}`);
|
||||||
|
console.log(`Admin Email: ${ADMIN_EMAIL}`);
|
||||||
|
console.log(`Website Name: ${WEBSITE_NAME}`);
|
||||||
|
console.log(`Mode: ${isProduction ? 'production' : 'development'}`);
|
||||||
|
console.log('--------------------');
|
||||||
|
|
||||||
|
// Create a transporter
|
||||||
|
function createTransporter(options = {}) {
|
||||||
|
const isProtonMail = SMTP_HOST.includes('protonmail');
|
||||||
|
|
||||||
|
return nodemailer.createTransport({
|
||||||
|
host: SMTP_HOST,
|
||||||
|
port: parseInt(SMTP_PORT, 10),
|
||||||
|
secure: parseInt(SMTP_PORT, 10) === 465,
|
||||||
|
auth: {
|
||||||
|
user: SMTP_USER,
|
||||||
|
pass: SMTP_PASS,
|
||||||
|
},
|
||||||
|
...(isProtonMail && {
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
ciphers: 'SSLv3'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
debug: true,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test different transporter configurations
|
||||||
|
async function runTests() {
|
||||||
|
try {
|
||||||
|
// Test 1: Basic configuration
|
||||||
|
console.log('\nTest 1: Basic configuration');
|
||||||
|
const transporter1 = createTransporter();
|
||||||
|
await testTransporter(transporter1, 'Basic configuration');
|
||||||
|
|
||||||
|
// Test 2: With secure=false explicitly set
|
||||||
|
console.log('\nTest 2: With secure=false explicitly set');
|
||||||
|
const transporter2 = createTransporter({ secure: false });
|
||||||
|
await testTransporter(transporter2, 'secure=false configuration');
|
||||||
|
|
||||||
|
// Test 3: With requireTLS=true
|
||||||
|
console.log('\nTest 3: With requireTLS=true');
|
||||||
|
const transporter3 = createTransporter({ requireTLS: true });
|
||||||
|
await testTransporter(transporter3, 'requireTLS=true configuration');
|
||||||
|
|
||||||
|
// Test 4: With different from address
|
||||||
|
console.log('\nTest 4: With different from address');
|
||||||
|
const transporter4 = createTransporter();
|
||||||
|
await testTransporter(
|
||||||
|
transporter4,
|
||||||
|
'Different from address',
|
||||||
|
{ from: `"Test" <${SMTP_USER}>` }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('\nAll tests completed!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error running tests:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test a specific transporter configuration
|
||||||
|
async function testTransporter(transporter, testName, options = {}) {
|
||||||
|
console.log(`Testing ${testName}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify connection
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
transporter.verify((error, success) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(`Connection test failed for ${testName}:`, error);
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
console.log(`Connection successful for ${testName}`);
|
||||||
|
resolve(success);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send test email
|
||||||
|
const fromAddress = options.from || `"${WEBSITE_NAME}" <${SMTP_USER}>`;
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: fromAddress,
|
||||||
|
to: ADMIN_EMAIL,
|
||||||
|
subject: `Production Email Test: ${testName}`,
|
||||||
|
text: `This is a test email from the production-email-test.cjs script using ${testName}.\n\nTime: ${new Date().toISOString()}`,
|
||||||
|
html: `
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee;">
|
||||||
|
<h2>Production Email Test: ${testName}</h2>
|
||||||
|
<p>This is a test email from the production-email-test.cjs script using ${testName}.</p>
|
||||||
|
<p>Time: ${new Date().toISOString()}</p>
|
||||||
|
<p>Configuration:</p>
|
||||||
|
<ul>
|
||||||
|
<li>SMTP Host: ${SMTP_HOST}</li>
|
||||||
|
<li>SMTP Port: ${SMTP_PORT}</li>
|
||||||
|
<li>From: ${fromAddress}</li>
|
||||||
|
<li>To: ${ADMIN_EMAIL}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Email sent successfully for ${testName}!`);
|
||||||
|
console.log(`Message ID: ${info.messageId}`);
|
||||||
|
console.log(`Response: ${info.response}`);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in ${testName}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the tests
|
||||||
|
runTests().catch(error => {
|
||||||
|
console.error('Unhandled error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
252
public/test-contact-form.html
Normal file
252
public/test-contact-form.html
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Contact Form Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.debug {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
#debug-log {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Contact Form Test</h1>
|
||||||
|
|
||||||
|
<div id="success-message" class="success">
|
||||||
|
Your message has been sent successfully!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="error-message" class="error">
|
||||||
|
There was an error sending your message. Please try again.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="contact-form">
|
||||||
|
<input type="hidden" name="csrf_token" id="csrf_token" value="">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name:</label>
|
||||||
|
<input type="text" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="message">Message:</label>
|
||||||
|
<textarea id="message" name="message" rows="5" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="disclaimer" name="disclaimer" required>
|
||||||
|
I agree to the terms and conditions
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Send Message</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="debug">
|
||||||
|
<h3>Debug Log</h3>
|
||||||
|
<div id="debug-log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Debug logging function
|
||||||
|
function log(message) {
|
||||||
|
const debugLog = document.getElementById('debug-log');
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
debugLog.innerHTML += `[${timestamp}] ${message}\n`;
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch CSRF token
|
||||||
|
async function fetchCsrfToken() {
|
||||||
|
log('Fetching CSRF token...');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/contact?csrf=true');
|
||||||
|
log(`CSRF response status: ${response.status}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
log(`CSRF request failed: ${response.statusText}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
log(`CSRF response text: ${text}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
log(`CSRF token received: ${data.csrfToken ? 'yes' : 'no'}`);
|
||||||
|
return data.csrfToken;
|
||||||
|
} catch (parseError) {
|
||||||
|
log(`Error parsing CSRF response: ${parseError.message}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error fetching CSRF token: ${error.message}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize form
|
||||||
|
async function initForm() {
|
||||||
|
log('Initializing form...');
|
||||||
|
const csrfToken = await fetchCsrfToken();
|
||||||
|
|
||||||
|
if (csrfToken) {
|
||||||
|
document.getElementById('csrf_token').value = csrfToken;
|
||||||
|
log('CSRF token set in form');
|
||||||
|
} else {
|
||||||
|
log('Failed to get CSRF token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values for testing
|
||||||
|
document.getElementById('name').value = 'Test User';
|
||||||
|
document.getElementById('email').value = 'richard@bergsma.it';
|
||||||
|
document.getElementById('message').value = 'This is a test message from the test-contact-form.html page. ' + new Date().toISOString();
|
||||||
|
|
||||||
|
log('Form initialized with test values');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
async function handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
log('Form submitted');
|
||||||
|
|
||||||
|
// Reset messages
|
||||||
|
document.getElementById('success-message').style.display = 'none';
|
||||||
|
document.getElementById('error-message').style.display = 'none';
|
||||||
|
|
||||||
|
// Get form data
|
||||||
|
const form = document.getElementById('contact-form');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
// Log form data
|
||||||
|
log('Form data:');
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
log(`${key}: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
formData.append('timestamp', Date.now().toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
log('Sending form data to /api/contact');
|
||||||
|
const response = await fetch('/api/contact', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`Response status: ${response.status}`);
|
||||||
|
const text = await response.text();
|
||||||
|
log(`Response text: ${text}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(text);
|
||||||
|
log(`Response parsed: ${JSON.stringify(result)}`);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
log('Form submission successful');
|
||||||
|
document.getElementById('success-message').style.display = 'block';
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
// Get a new CSRF token
|
||||||
|
const newToken = await fetchCsrfToken();
|
||||||
|
if (newToken) {
|
||||||
|
document.getElementById('csrf_token').value = newToken;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`Form submission failed: ${JSON.stringify(result.errors || {})}`);
|
||||||
|
document.getElementById('error-message').style.display = 'block';
|
||||||
|
|
||||||
|
if (result.errors && result.errors.csrf) {
|
||||||
|
log('CSRF token invalid, getting new token');
|
||||||
|
const newToken = await fetchCsrfToken();
|
||||||
|
if (newToken) {
|
||||||
|
document.getElementById('csrf_token').value = newToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
log(`Error parsing response: ${parseError.message}`);
|
||||||
|
document.getElementById('error-message').style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error submitting form: ${error.message}`);
|
||||||
|
document.getElementById('error-message').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
log('DOM loaded');
|
||||||
|
initForm();
|
||||||
|
|
||||||
|
// Add form submit handler
|
||||||
|
document.getElementById('contact-form').addEventListener('submit', handleSubmit);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -465,12 +465,22 @@ import { UI } from 'astrowind:config';
|
|||||||
|
|
||||||
// Fetch CSRF token from the server
|
// Fetch CSRF token from the server
|
||||||
async function fetchCsrfToken() {
|
async function fetchCsrfToken() {
|
||||||
|
console.log('Fetching CSRF token');
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/contact?csrf=true');
|
const response = await fetch('/api/contact?csrf=true');
|
||||||
|
console.log('CSRF response status:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('CSRF request failed:', response.statusText);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('CSRF token received:', data.csrfToken ? 'yes' : 'no');
|
||||||
return data.csrfToken;
|
return data.csrfToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching CSRF token:', error);
|
console.error('Error fetching CSRF token:', error);
|
||||||
|
console.error('Error details:', error.message);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,15 +499,19 @@ import { UI } from 'astrowind:config';
|
|||||||
// Form validation and submission
|
// Form validation and submission
|
||||||
contactForm.addEventListener('submit', async function(e) {
|
contactForm.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log('Form submitted');
|
||||||
|
|
||||||
// Reset previous error messages
|
// Reset previous error messages
|
||||||
resetFormErrors();
|
resetFormErrors();
|
||||||
|
|
||||||
// Client-side validation
|
// Client-side validation
|
||||||
if (!validateForm(contactForm)) {
|
if (!validateForm(contactForm)) {
|
||||||
|
console.log('Form validation failed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Form validation passed');
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
const submitButton = contactForm.querySelector('button[type="submit"]');
|
const submitButton = contactForm.querySelector('button[type="submit"]');
|
||||||
const originalButtonText = submitButton.innerHTML;
|
const originalButtonText = submitButton.innerHTML;
|
||||||
@@ -507,9 +521,16 @@ import { UI } from 'astrowind:config';
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData(contactForm);
|
const formData = new FormData(contactForm);
|
||||||
|
|
||||||
|
// Log form data
|
||||||
|
console.log('Form data:');
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
console.log(`${key}: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Add timestamp to help prevent duplicate submissions
|
// Add timestamp to help prevent duplicate submissions
|
||||||
formData.append('timestamp', Date.now().toString());
|
formData.append('timestamp', Date.now().toString());
|
||||||
|
|
||||||
|
console.log('Sending form data to /api/contact');
|
||||||
const response = await fetch('/api/contact', {
|
const response = await fetch('/api/contact', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
@@ -518,23 +539,40 @@ import { UI } from 'astrowind:config';
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Response status:', response.status);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
console.log('Response data:', result);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
console.log('Form submission successful');
|
||||||
// Show success message
|
// Show success message
|
||||||
document.getElementById('form-success').classList.remove('hidden');
|
document.getElementById('form-success').classList.remove('hidden');
|
||||||
contactForm.reset();
|
contactForm.reset();
|
||||||
|
|
||||||
// Get a new CSRF token for next submission
|
// Get a new CSRF token for next submission
|
||||||
|
console.log('Getting new CSRF token');
|
||||||
const newToken = await fetchCsrfToken();
|
const newToken = await fetchCsrfToken();
|
||||||
|
console.log('New CSRF token:', newToken ? 'received' : 'not received');
|
||||||
if (csrfTokenInput) {
|
if (csrfTokenInput) {
|
||||||
csrfTokenInput.value = newToken;
|
csrfTokenInput.value = newToken;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Show error messages
|
console.log('Form submission failed:', result);
|
||||||
document.getElementById('form-error').classList.remove('hidden');
|
// Show specific error messages instead of generic form error
|
||||||
|
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
|
console.log('Form errors:', result.errors);
|
||||||
|
|
||||||
|
// Only show the generic error if there are no field-specific errors
|
||||||
|
// or if there's a server error not related to a specific field
|
||||||
|
const hasFieldErrors = Object.keys(result.errors).some(field =>
|
||||||
|
['name', 'email', 'message', 'disclaimer', 'csrf'].includes(field)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasFieldErrors) {
|
||||||
|
document.getElementById('form-error').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display field-specific errors
|
||||||
Object.keys(result.errors).forEach(field => {
|
Object.keys(result.errors).forEach(field => {
|
||||||
const inputElement = contactForm.querySelector(`[name="${field}"]`);
|
const inputElement = contactForm.querySelector(`[name="${field}"]`);
|
||||||
if (inputElement) {
|
if (inputElement) {
|
||||||
@@ -543,13 +581,25 @@ import { UI } from 'astrowind:config';
|
|||||||
feedbackElement.textContent = result.errors[field];
|
feedbackElement.textContent = result.errors[field];
|
||||||
feedbackElement.classList.remove('hidden');
|
feedbackElement.classList.remove('hidden');
|
||||||
inputElement.classList.add('border-red-500');
|
inputElement.classList.add('border-red-500');
|
||||||
|
|
||||||
|
// Special handling for checkbox
|
||||||
|
if (field === 'disclaimer') {
|
||||||
|
const checkboxContainer = inputElement.closest('.flex.items-start');
|
||||||
|
if (checkboxContainer) {
|
||||||
|
checkboxContainer.classList.add('checkbox-error');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// If no specific errors, show the generic error
|
||||||
|
document.getElementById('form-error').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If CSRF token is invalid, get a new one
|
// If CSRF token is invalid, get a new one
|
||||||
if (result.errors && result.errors.csrf) {
|
if (result.errors && result.errors.csrf) {
|
||||||
|
console.log('CSRF token invalid, getting new token');
|
||||||
const newToken = await fetchCsrfToken();
|
const newToken = await fetchCsrfToken();
|
||||||
if (csrfTokenInput) {
|
if (csrfTokenInput) {
|
||||||
csrfTokenInput.value = newToken;
|
csrfTokenInput.value = newToken;
|
||||||
@@ -558,6 +608,7 @@ import { UI } from 'astrowind:config';
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting form:', error);
|
console.error('Error submitting form:', error);
|
||||||
|
console.error('Error details:', error.message);
|
||||||
document.getElementById('form-error').classList.remove('hidden');
|
document.getElementById('form-error').classList.remove('hidden');
|
||||||
} finally {
|
} finally {
|
||||||
// Restore button state
|
// Restore button state
|
||||||
@@ -608,6 +659,30 @@ import { UI } from 'astrowind:config';
|
|||||||
}
|
}
|
||||||
input.classList.remove('border-red-500');
|
input.classList.remove('border-red-500');
|
||||||
|
|
||||||
|
// Checkbox validation (special case for disclaimer)
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
if (input.required && !input.checked) {
|
||||||
|
isValid = false;
|
||||||
|
if (feedbackElement) {
|
||||||
|
feedbackElement.textContent = 'Please check the required consent box before submitting';
|
||||||
|
feedbackElement.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
input.classList.add('border-red-500');
|
||||||
|
// Add red border to the checkbox container for better visibility
|
||||||
|
const checkboxContainer = input.closest('.flex.items-start');
|
||||||
|
if (checkboxContainer) {
|
||||||
|
checkboxContainer.classList.add('checkbox-error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove error styling when checkbox is checked
|
||||||
|
const checkboxContainer = input.closest('.flex.items-start');
|
||||||
|
if (checkboxContainer) {
|
||||||
|
checkboxContainer.classList.remove('checkbox-error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if empty
|
// Check if empty
|
||||||
if (input.required && !input.value.trim()) {
|
if (input.required && !input.value.trim()) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
@@ -655,6 +730,11 @@ import { UI } from 'astrowind:config';
|
|||||||
input.classList.remove('border-red-500');
|
input.classList.remove('border-red-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove checkbox container error styling
|
||||||
|
document.querySelectorAll('.flex.items-start').forEach(container => {
|
||||||
|
container.classList.remove('checkbox-error');
|
||||||
|
});
|
||||||
|
|
||||||
// Hide form-level messages
|
// Hide form-level messages
|
||||||
document.getElementById('form-success')?.classList.add('hidden');
|
document.getElementById('form-success')?.classList.add('hidden');
|
||||||
document.getElementById('form-error')?.classList.add('hidden');
|
document.getElementById('form-error')?.classList.add('hidden');
|
||||||
|
@@ -5,6 +5,16 @@ import Button from '~/components/ui/Button.astro';
|
|||||||
const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
|
const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Checkbox error styling */
|
||||||
|
.checkbox-error {
|
||||||
|
border: 2px solid #ef4444;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: rgba(239, 68, 68, 0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<form id="contact-form" action="/api/contact" method="POST" class="needs-validation" novalidate>
|
<form id="contact-form" action="/api/contact" method="POST" class="needs-validation" novalidate>
|
||||||
|
|
||||||
<!-- Form status messages -->
|
<!-- Form status messages -->
|
||||||
@@ -13,7 +23,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="form-error" class="hidden mb-6 p-4 bg-red-100 border border-red-200 text-red-700 rounded-lg">
|
<div id="form-error" class="hidden mb-6 p-4 bg-red-100 border border-red-200 text-red-700 rounded-lg">
|
||||||
There was an error sending your message. Please try again.
|
There was an error sending your message. Please check all fields and try again.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CSRF Token - Will be populated via JavaScript -->
|
<!-- CSRF Token - Will be populated via JavaScript -->
|
||||||
|
@@ -8,7 +8,7 @@ interface AdminNotificationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAdminNotificationSubject(): string {
|
export function getAdminNotificationSubject(): string {
|
||||||
return 'New Contact Form Submission from bergsma.it';
|
return `New Contact Form Submission from ${process.env.WEBSITE_NAME || '365devnet.eu'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAdminNotificationHtml(props: AdminNotificationProps): string {
|
export function getAdminNotificationHtml(props: AdminNotificationProps): string {
|
||||||
|
@@ -7,17 +7,17 @@ interface UserConfirmationProps {
|
|||||||
contactEmail?: string;
|
contactEmail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserConfirmationSubject(websiteName: string = 'bergsma.it'): string {
|
export function getUserConfirmationSubject(websiteName: string = process.env.WEBSITE_NAME || '365devnet.eu'): string {
|
||||||
return `Thank you for contacting ${websiteName}`;
|
return `Thank you for contacting ${websiteName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserConfirmationHtml(props: UserConfirmationProps): string {
|
export function getUserConfirmationHtml(props: UserConfirmationProps): string {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
message,
|
message,
|
||||||
submittedAt,
|
submittedAt,
|
||||||
websiteName = 'bergsma.it',
|
websiteName = process.env.WEBSITE_NAME || '365devnet.eu',
|
||||||
contactEmail = 'richard@bergsma.it'
|
contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it'
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -92,12 +92,12 @@ export function getUserConfirmationHtml(props: UserConfirmationProps): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getUserConfirmationText(props: UserConfirmationProps): string {
|
export function getUserConfirmationText(props: UserConfirmationProps): string {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
message,
|
message,
|
||||||
submittedAt,
|
submittedAt,
|
||||||
websiteName = 'bergsma.it',
|
websiteName = process.env.WEBSITE_NAME || '365devnet.eu',
|
||||||
contactEmail = 'richard@bergsma.it'
|
contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it'
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
@@ -162,20 +162,26 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
|||||||
errors.csrf = 'Invalid or expired security token. Please refresh the page and try again.';
|
errors.csrf = 'Invalid or expired security token. Please refresh the page and try again.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name || name.length < 2) {
|
if (!name) {
|
||||||
errors.name = 'Name is required and must be at least 2 characters';
|
errors.name = 'Please enter your name';
|
||||||
|
} else if (name.length < 2) {
|
||||||
|
errors.name = 'Your name must be at least 2 characters long';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email || !isValidEmail(email)) {
|
if (!email) {
|
||||||
errors.email = 'A valid email address is required';
|
errors.email = 'Please enter your email address';
|
||||||
|
} else if (!isValidEmail(email)) {
|
||||||
|
errors.email = 'Please enter a valid email address (e.g., name@example.com)';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message || message.length < 10) {
|
if (!message) {
|
||||||
errors.message = 'Message is required and must be at least 10 characters';
|
errors.message = 'Please enter your message';
|
||||||
|
} else if (message.length < 10) {
|
||||||
|
errors.message = 'Your message must be at least 10 characters long';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disclaimer) {
|
if (!disclaimer) {
|
||||||
errors.disclaimer = 'You must agree to the disclaimer';
|
errors.disclaimer = 'Please check the required consent box before submitting';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for spam
|
// Check for spam
|
||||||
|
@@ -13,11 +13,12 @@ const {
|
|||||||
SMTP_PASS = '',
|
SMTP_PASS = '',
|
||||||
ADMIN_EMAIL = 'richard@bergsma.it',
|
ADMIN_EMAIL = 'richard@bergsma.it',
|
||||||
WEBSITE_NAME = 'bergsma.it',
|
WEBSITE_NAME = 'bergsma.it',
|
||||||
NODE_ENV = 'development'
|
NODE_ENV = 'production'
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
// Email configuration
|
// Email configuration
|
||||||
const isProduction = NODE_ENV === 'production';
|
// Force production mode for testing
|
||||||
|
const isProduction = true; // NODE_ENV === 'production';
|
||||||
|
|
||||||
// Create a transporter for sending emails
|
// Create a transporter for sending emails
|
||||||
let transporter: nodemailer.Transporter;
|
let transporter: nodemailer.Transporter;
|
||||||
@@ -31,6 +32,15 @@ function initializeTransporter() {
|
|||||||
// ProtonMail often requires using their Bridge application for SMTP
|
// ProtonMail often requires using their Bridge application for SMTP
|
||||||
const isProtonMail = SMTP_HOST.includes('protonmail');
|
const isProtonMail = SMTP_HOST.includes('protonmail');
|
||||||
|
|
||||||
|
// Log the email configuration
|
||||||
|
console.log('Initializing email transporter with:');
|
||||||
|
console.log(`SMTP Host: ${SMTP_HOST}`);
|
||||||
|
console.log(`SMTP Port: ${SMTP_PORT}`);
|
||||||
|
console.log(`SMTP User: ${SMTP_USER}`);
|
||||||
|
console.log(`Admin Email: ${ADMIN_EMAIL}`);
|
||||||
|
console.log(`Website Name: ${WEBSITE_NAME}`);
|
||||||
|
console.log(`Environment: ${NODE_ENV}`);
|
||||||
|
|
||||||
transporter = nodemailer.createTransport({
|
transporter = nodemailer.createTransport({
|
||||||
host: SMTP_HOST,
|
host: SMTP_HOST,
|
||||||
port: parseInt(SMTP_PORT, 10),
|
port: parseInt(SMTP_PORT, 10),
|
||||||
@@ -179,8 +189,13 @@ export async function sendEmail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ensure from address matches SMTP_USER for ProtonMail
|
||||||
|
const fromAddress = isProduction ?
|
||||||
|
`"${WEBSITE_NAME}" <${SMTP_USER}>` :
|
||||||
|
`"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`;
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`,
|
from: fromAddress,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
@@ -188,7 +203,7 @@ export async function sendEmail(
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log('Mail options:', {
|
console.log('Mail options:', {
|
||||||
from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`,
|
from: fromAddress,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
textLength: text.length,
|
textLength: text.length,
|
||||||
@@ -199,6 +214,15 @@ export async function sendEmail(
|
|||||||
const info = await transporter.sendMail(mailOptions);
|
const info = await transporter.sendMail(mailOptions);
|
||||||
console.log('Email sent, info:', info.messageId);
|
console.log('Email sent, info:', info.messageId);
|
||||||
|
|
||||||
|
// Log additional information in production mode
|
||||||
|
if (isProduction) {
|
||||||
|
console.log('Email delivery details:', {
|
||||||
|
messageId: info.messageId,
|
||||||
|
response: info.response,
|
||||||
|
envelope: info.envelope
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
// In development, log the email content
|
// In development, log the email content
|
||||||
console.log('Email sent (development mode):');
|
console.log('Email sent (development mode):');
|
||||||
|
Reference in New Issue
Block a user