Update package-lock.json and enhance Form component for better error handling and user experience

- Changed "dev" to "devOptional" for several dependencies in package-lock.json to reflect updated package configurations.
- Improved the Form component by refining error handling during form submission, including specific messages for different error scenarios.
- Enhanced the spam detection logic and added clearer feedback for users during the manual review process.
- Updated the CSRF token handling to ensure it is set correctly upon form submission.
This commit is contained in:
2025-11-02 00:29:19 +01:00
parent 8773788335
commit 84a5e71b95
3 changed files with 92 additions and 34 deletions

21
package-lock.json generated
View File

@@ -2338,7 +2338,7 @@
"version": "0.3.6", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
@@ -4977,7 +4977,7 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/bulk-replace": { "node_modules/bulk-replace": {
@@ -6368,7 +6368,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"detect-libc": "bin/detect-libc.js" "detect-libc": "bin/detect-libc.js"
@@ -8804,7 +8804,7 @@
"version": "1.21.7", "version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
@@ -9024,7 +9024,7 @@
"version": "1.28.2", "version": "1.28.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.28.2.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.28.2.tgz",
"integrity": "sha512-ePLRrbt3fgjXI5VFZOLbvkLD5ZRuxGKm+wJ3ujCqBtL3NanDHPo/5zicR5uEKAPiIjBYF99BM4K4okvMznjkVA==", "integrity": "sha512-ePLRrbt3fgjXI5VFZOLbvkLD5ZRuxGKm+wJ3ujCqBtL3NanDHPo/5zicR5uEKAPiIjBYF99BM4K4okvMznjkVA==",
"dev": true, "devOptional": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"detect-libc": "^1.0.3" "detect-libc": "^1.0.3"
@@ -13039,7 +13039,7 @@
"version": "0.5.21", "version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
@@ -13050,7 +13050,7 @@
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true, "devOptional": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -13558,7 +13558,7 @@
"version": "5.37.0", "version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"dev": true, "devOptional": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
@@ -13577,7 +13577,7 @@
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/thenify": { "node_modules/thenify": {
@@ -13819,7 +13819,6 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -14950,7 +14949,7 @@
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"dev": true, "devOptional": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"

View File

@@ -84,12 +84,12 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
{ {
disclaimer && ( disclaimer && (
<div class="mt-3 flex items-start mb-6"> <div class="mt-3 flex items-start mb-6">
<div class="flex mt-0.5"> <div class="flex items-center h-5">
<input <input
id="disclaimer" id="disclaimer"
name="disclaimer" name="disclaimer"
type="checkbox" type="checkbox"
class="cursor-pointer mt-1 py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900" class="w-4 h-4 cursor-pointer rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-slate-700 dark:checked:bg-blue-600"
required required
aria-describedby="invalid-feedback-disclaimer" aria-describedby="invalid-feedback-disclaimer"
/> />
@@ -163,13 +163,10 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
<div id="manual-review-result" class="mt-4 text-center text-green-700 dark:text-green-400 font-medium"></div> <div id="manual-review-result" class="mt-4 text-center text-green-700 dark:text-green-400 font-medium"></div>
</div> </div>
<script> <script define:vars={{ spamReviewConfig: spamReview }}>
// TypeScript: declare the property on window // Make spamReview accessible in client script
declare global { const spamReview = spamReviewConfig || {};
interface Window {
__originalEmail?: string;
}
}
async function setCsrfToken() { async function setCsrfToken() {
try { try {
const res = await fetch('/api/contact?csrf=true'); const res = await fetch('/api/contact?csrf=true');
@@ -177,7 +174,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
const data = await res.json(); const data = await res.json();
const csrfInput = document.getElementById('csrf_token'); const csrfInput = document.getElementById('csrf_token');
if (csrfInput && data.csrfToken) { if (csrfInput && data.csrfToken) {
(csrfInput as HTMLInputElement).value = data.csrfToken; csrfInput.value = data.csrfToken;
} }
} }
} catch (e) { } catch (e) {
@@ -187,7 +184,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
document.addEventListener('DOMContentLoaded', setCsrfToken); document.addEventListener('DOMContentLoaded', setCsrfToken);
const form = document.getElementById('contact-form') as HTMLFormElement | null; const form = document.getElementById('contact-form');
if (form) { if (form) {
form.addEventListener('submit', async (event) => { form.addEventListener('submit', async (event) => {
@@ -198,14 +195,29 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
response = await fetch('/api/contact', { response = await fetch('/api/contact', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: {
'Accept': 'application/json',
},
}); });
result = await response.json();
// Check if response is JSON before parsing
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
result = await response.json();
} else {
const text = await response.text();
throw new Error(`Unexpected response type: ${contentType || 'unknown'}. Response: ${text.substring(0, 100)}`);
}
console.log('Contact API response:', result); console.log('Contact API response:', result);
} catch (_err) { } catch (err) {
const errorElement = document.getElementById('form-error'); const errorElement = document.getElementById('form-error');
if (errorElement) errorElement.classList.remove('hidden');
const successElement = document.getElementById('form-success'); const successElement = document.getElementById('form-success');
if (successElement) successElement.classList.add('hidden'); if (successElement) successElement.classList.add('hidden');
if (errorElement) {
errorElement.textContent = 'Network error. Please check your connection and try again.';
errorElement.classList.remove('hidden');
}
console.error('Form submission error:', err);
return; return;
} }
@@ -214,8 +226,8 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
console.log('Spam detected, showing manual review UI.'); console.log('Spam detected, showing manual review UI.');
form.style.display = 'none'; form.style.display = 'none';
const spamWarning = document.getElementById('spam-warning'); const spamWarning = document.getElementById('spam-warning');
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null; const manualEmail = document.getElementById('manual-email');
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null; const manualToken = document.getElementById('manual-token');
// Store the original email in a variable (not in the input) // Store the original email in a variable (not in the input)
window.__originalEmail = String(formData.get('email')); window.__originalEmail = String(formData.get('email'));
if (spamWarning && manualEmail && manualToken) { if (spamWarning && manualEmail && manualToken) {
@@ -228,17 +240,64 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
if (!response.ok) { if (!response.ok) {
const errorElement = document.getElementById('form-error'); const errorElement = document.getElementById('form-error');
if (errorElement) errorElement.classList.remove('hidden');
const successElement = document.getElementById('form-success'); const successElement = document.getElementById('form-success');
if (successElement) successElement.classList.add('hidden'); if (successElement) successElement.classList.add('hidden');
// Handle specific error cases
if (result.message && result.message.includes('Please use POST')) {
if (errorElement) {
errorElement.textContent = 'There was an error with the form submission. Please refresh the page and try again.';
errorElement.classList.remove('hidden');
}
} else if (result.errors) {
// Handle field-specific validation errors
if (errorElement) {
errorElement.textContent = 'Please check all fields and try again.';
errorElement.classList.remove('hidden');
}
// Show field-specific errors
Object.keys(result.errors).forEach((fieldName) => {
const field = form.querySelector(`[name="${fieldName}"]`);
const feedback = document.getElementById(`invalid-feedback-${fieldName}`);
if (field && feedback) {
field.classList.add('border-red-500');
feedback.textContent = result.errors[fieldName];
feedback.classList.remove('hidden');
}
});
} else if (result.error) {
if (errorElement) {
errorElement.textContent = result.error;
errorElement.classList.remove('hidden');
}
} else {
if (errorElement) {
errorElement.textContent = 'There was an error sending your message. Please try again.';
errorElement.classList.remove('hidden');
}
}
return; return;
} }
// Success // Success
const successElement = document.getElementById('form-success'); const successElement = document.getElementById('form-success');
if (successElement) successElement.classList.remove('hidden'); if (successElement) {
successElement.textContent = result.message || 'Your message has been sent successfully. We will get back to you soon!';
successElement.classList.remove('hidden');
}
const errorElement = document.getElementById('form-error'); const errorElement = document.getElementById('form-error');
if (errorElement) errorElement.classList.add('hidden'); if (errorElement) errorElement.classList.add('hidden');
// Clear any field error states
form.querySelectorAll('.border-red-500').forEach((el) => {
el.classList.remove('border-red-500');
});
form.querySelectorAll('.invalid-feedback').forEach((el) => {
el.classList.add('hidden');
});
form.reset(); form.reset();
setCsrfToken(); setCsrfToken();
}); });
@@ -249,11 +308,11 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '', s
if (manualReviewForm) { if (manualReviewForm) {
manualReviewForm.onsubmit = async function (e) { manualReviewForm.onsubmit = async function (e) {
e.preventDefault(); e.preventDefault();
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null; const manualEmail = document.getElementById('manual-email');
const manualJustification = document.getElementById('manual-justification') as HTMLTextAreaElement | null; const manualJustification = document.getElementById('manual-justification');
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null; const manualToken = document.getElementById('manual-token');
const resultDiv = document.getElementById('manual-review-result'); const resultDiv = document.getElementById('manual-review-result');
const form = document.getElementById('contact-form') as HTMLFormElement | null; const form = document.getElementById('contact-form');
const spamWarning = document.getElementById('spam-warning'); const spamWarning = document.getElementById('spam-warning');
if (!manualEmail || !manualJustification || !manualToken || !resultDiv) return; if (!manualEmail || !manualJustification || !manualToken || !resultDiv) return;
const email = manualEmail.value; const email = manualEmail.value;

View File

@@ -10,7 +10,7 @@ export async function isSpamWithGemini(message: string): Promise<boolean> {
console.warn('[Gemini] API key not set; skipping spam check.'); console.warn('[Gemini] API key not set; skipping spam check.');
return false; return false;
} }
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
const prompt = `Is the following message spam? Reply with only 'yes' or 'no'.\n\nMessage:\n${message}`; const prompt = `Is the following message spam? Reply with only 'yes' or 'no'.\n\nMessage:\n${message}`;
const result = await model.generateContent(prompt); const result = await model.generateContent(prompt);
const response = result.response.text().trim().toLowerCase(); const response = result.response.text().trim().toLowerCase();