Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a68c112cb | ||
|
|
5898e97a27 | ||
|
|
fd024874c7 | ||
|
|
683ff54cdb | ||
|
|
da0e2367c0 | ||
|
|
40ed89793b | ||
|
|
48058c544e | ||
|
|
0213226882 | ||
|
|
4d882f1dc7 | ||
|
|
68e1623c1b | ||
|
|
d002516bc8 | ||
|
|
3c46684b1b | ||
|
|
8f6e530dc4 | ||
|
|
9fcf2e73fe | ||
|
|
c7bc4c724f | ||
|
|
91c9b2e69b | ||
|
|
45cc2814b9 | ||
|
|
c2677881a7 |
@@ -1,7 +1,7 @@
|
|||||||
# Graph Report - /home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f (2026-04-12)
|
# Graph Report - /home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f (2026-04-13)
|
||||||
|
|
||||||
## Corpus Check
|
## Corpus Check
|
||||||
- 43 files · ~210,827 words
|
- 43 files · ~211,373 words
|
||||||
- Verdict: corpus is large enough that graph structure adds value.
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|||||||
+1
File diff suppressed because one or more lines are too long
+16
-16
@@ -103,7 +103,7 @@
|
|||||||
"label": "sendContactEmail()",
|
"label": "sendContactEmail()",
|
||||||
"file_type": "code",
|
"file_type": "code",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L96",
|
"source_location": "L150",
|
||||||
"id": "contact_sendcontactemail",
|
"id": "contact_sendcontactemail",
|
||||||
"community": 5
|
"community": 5
|
||||||
},
|
},
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
"label": "escapeHtml()",
|
"label": "escapeHtml()",
|
||||||
"file_type": "code",
|
"file_type": "code",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L181",
|
"source_location": "L288",
|
||||||
"id": "contact_escapehtml",
|
"id": "contact_escapehtml",
|
||||||
"community": 5
|
"community": 5
|
||||||
},
|
},
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"label": "corsHeaders()",
|
"label": "corsHeaders()",
|
||||||
"file_type": "code",
|
"file_type": "code",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L195",
|
"source_location": "L302",
|
||||||
"id": "contact_corsheaders",
|
"id": "contact_corsheaders",
|
||||||
"community": 5
|
"community": 5
|
||||||
},
|
},
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"label": "OPTIONS()",
|
"label": "OPTIONS()",
|
||||||
"file_type": "code",
|
"file_type": "code",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L220",
|
"source_location": "L327",
|
||||||
"id": "contact_options",
|
"id": "contact_options",
|
||||||
"community": 5
|
"community": 5
|
||||||
},
|
},
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
"label": "POST()",
|
"label": "POST()",
|
||||||
"file_type": "code",
|
"file_type": "code",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L227",
|
"source_location": "L334",
|
||||||
"id": "contact_post",
|
"id": "contact_post",
|
||||||
"community": 5
|
"community": 5
|
||||||
},
|
},
|
||||||
@@ -1421,7 +1421,7 @@
|
|||||||
"relation": "contains",
|
"relation": "contains",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L96",
|
"source_location": "L150",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact",
|
"_src": "contact",
|
||||||
"_tgt": "contact_sendcontactemail",
|
"_tgt": "contact_sendcontactemail",
|
||||||
@@ -1433,7 +1433,7 @@
|
|||||||
"relation": "contains",
|
"relation": "contains",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L181",
|
"source_location": "L288",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact",
|
"_src": "contact",
|
||||||
"_tgt": "contact_escapehtml",
|
"_tgt": "contact_escapehtml",
|
||||||
@@ -1445,7 +1445,7 @@
|
|||||||
"relation": "contains",
|
"relation": "contains",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L195",
|
"source_location": "L302",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact",
|
"_src": "contact",
|
||||||
"_tgt": "contact_corsheaders",
|
"_tgt": "contact_corsheaders",
|
||||||
@@ -1457,7 +1457,7 @@
|
|||||||
"relation": "contains",
|
"relation": "contains",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L220",
|
"source_location": "L327",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact",
|
"_src": "contact",
|
||||||
"_tgt": "contact_options",
|
"_tgt": "contact_options",
|
||||||
@@ -1469,7 +1469,7 @@
|
|||||||
"relation": "contains",
|
"relation": "contains",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L227",
|
"source_location": "L334",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact",
|
"_src": "contact",
|
||||||
"_tgt": "contact_post",
|
"_tgt": "contact_post",
|
||||||
@@ -1481,7 +1481,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L233",
|
"source_location": "L340",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact_post",
|
"_src": "contact_post",
|
||||||
"_tgt": "contact_checkratelimit",
|
"_tgt": "contact_checkratelimit",
|
||||||
@@ -1493,7 +1493,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L297",
|
"source_location": "L404",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact_post",
|
"_src": "contact_post",
|
||||||
"_tgt": "contact_validatecontactform",
|
"_tgt": "contact_validatecontactform",
|
||||||
@@ -1505,7 +1505,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L155",
|
"source_location": "L221",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact_sendcontactemail",
|
"_src": "contact_sendcontactemail",
|
||||||
"_tgt": "contact_escapehtml",
|
"_tgt": "contact_escapehtml",
|
||||||
@@ -1517,7 +1517,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L307",
|
"source_location": "L417",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact_post",
|
"_src": "contact_post",
|
||||||
"_tgt": "contact_sendcontactemail",
|
"_tgt": "contact_sendcontactemail",
|
||||||
@@ -1529,7 +1529,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L221",
|
"source_location": "L328",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact_options",
|
"_src": "contact_options",
|
||||||
"_tgt": "contact_corsheaders",
|
"_tgt": "contact_corsheaders",
|
||||||
@@ -1541,7 +1541,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||||
"source_location": "L235",
|
"source_location": "L342",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "contact_post",
|
"_src": "contact_post",
|
||||||
"_tgt": "contact_corsheaders",
|
"_tgt": "contact_corsheaders",
|
||||||
|
|||||||
Generated
+13
@@ -13,6 +13,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"astro": "^4.5.0",
|
"astro": "^4.5.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"nodemailer": "^8.0.3"
|
"nodemailer": "^8.0.3"
|
||||||
},
|
},
|
||||||
@@ -2812,6 +2813,18 @@
|
|||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||||
|
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dset": {
|
"node_modules/dset": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"astro": "^4.5.0",
|
"astro": "^4.5.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"nodemailer": "^8.0.3"
|
"nodemailer": "^8.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-3
@@ -1,12 +1,20 @@
|
|||||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
import { config } from 'dotenv';
|
||||||
import express from 'express';
|
|
||||||
import compression from 'compression';
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Load .env from the project root (same dir as server.mjs)
|
||||||
|
config({ path: join(__dirname, '.env') });
|
||||||
|
console.log('[server] Loaded .env from:', join(__dirname, '.env'));
|
||||||
|
console.log('[server] SMTP_HOST:', process.env.SMTP_HOST || 'NOT SET');
|
||||||
|
console.log('[server] CONTACT_EMAIL_TO:', process.env.CONTACT_EMAIL_TO || 'NOT SET');
|
||||||
|
|
||||||
|
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||||
|
import express from 'express';
|
||||||
|
import compression from 'compression';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Server configuration
|
// Server configuration
|
||||||
|
|||||||
+171
-34
@@ -90,15 +90,70 @@ function validateContactForm(data: ContactFormData): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SMTP transporter (created once at module level for reuse)
|
||||||
|
// ============================================================
|
||||||
|
let smtpTransporter: any = null;
|
||||||
|
let smtpVerified = false;
|
||||||
|
|
||||||
|
// Verify SMTP connection once at module initialization
|
||||||
|
(async () => {
|
||||||
|
const smtpHost = process.env.SMTP_HOST;
|
||||||
|
const smtpUser = process.env.SMTP_USER;
|
||||||
|
const smtpPass = process.env.SMTP_PASS;
|
||||||
|
const smtpPort = parseInt(process.env.SMTP_PORT ?? '587', 10);
|
||||||
|
|
||||||
|
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||||
|
logger.info('api.contact', 'SMTP not configured — skipping connection verification');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodemailer = await import('nodemailer');
|
||||||
|
smtpTransporter = nodemailer.default.createTransport({
|
||||||
|
host: smtpHost,
|
||||||
|
port: smtpPort,
|
||||||
|
secure: false, // Port 587 uses STARTTLS, not SSL
|
||||||
|
auth: { user: smtpUser, pass: smtpPass },
|
||||||
|
requireTLS: true, // Force STARTTLS upgrade
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false, // Handle Hostinger certificate issues
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpTransporter.verify();
|
||||||
|
smtpVerified = true;
|
||||||
|
console.log('SMTP connection verified successfully');
|
||||||
|
logger.info('api.contact', 'SMTP connection verified successfully', {
|
||||||
|
host: smtpHost,
|
||||||
|
port: smtpPort,
|
||||||
|
user: smtpUser,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
smtpVerified = false;
|
||||||
|
console.error('SMTP connection verification failed:', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
});
|
||||||
|
logger.error('api.contact', 'SMTP connection verification failed', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Email sending via SMTP (nodemailer) or no-op in development
|
// Email sending via SMTP (nodemailer) or no-op in development
|
||||||
// ============================================================
|
// ============================================================
|
||||||
async function sendContactEmail(data: ContactFormData): Promise<void> {
|
async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||||
const smtpHost = import.meta.env.SMTP_HOST;
|
const smtpHost = process.env.SMTP_HOST;
|
||||||
const smtpUser = import.meta.env.SMTP_USER;
|
const smtpUser = process.env.SMTP_USER;
|
||||||
const smtpPass = import.meta.env.SMTP_PASS;
|
const smtpPass = process.env.SMTP_PASS;
|
||||||
const smtpPort = parseInt(import.meta.env.SMTP_PORT ?? '587', 10);
|
const smtpPort = parseInt(process.env.SMTP_PORT ?? '587', 10);
|
||||||
const recipientEmail = import.meta.env.CONTACT_EMAIL ?? import.meta.env.SMTP_USER;
|
const smtpFrom = process.env.SMTP_FROM;
|
||||||
|
const recipientEmail = process.env.CONTACT_EMAIL_TO;
|
||||||
|
|
||||||
if (!smtpHost || !smtpUser || !smtpPass || !recipientEmail) {
|
if (!smtpHost || !smtpUser || !smtpPass || !recipientEmail) {
|
||||||
// SMTP not configured - log and continue (graceful degradation)
|
// SMTP not configured - log and continue (graceful degradation)
|
||||||
@@ -112,13 +167,20 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
|||||||
|
|
||||||
// Dynamic import to avoid bundling issues when nodemailer isn't installed
|
// Dynamic import to avoid bundling issues when nodemailer isn't installed
|
||||||
try {
|
try {
|
||||||
const nodemailer = await import('nodemailer');
|
// Reuse module-level transporter if available, otherwise create a new one
|
||||||
const transporter = nodemailer.default.createTransport({
|
if (!smtpTransporter) {
|
||||||
host: smtpHost,
|
const nodemailer = await import('nodemailer');
|
||||||
port: smtpPort,
|
smtpTransporter = nodemailer.default.createTransport({
|
||||||
secure: smtpPort === 465,
|
host: smtpHost,
|
||||||
auth: { user: smtpUser, pass: smtpPass },
|
port: smtpPort,
|
||||||
});
|
secure: false,
|
||||||
|
auth: { user: smtpUser, pass: smtpPass },
|
||||||
|
requireTLS: true,
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const subjectLabels: Record<string, string> = {
|
const subjectLabels: Record<string, string> = {
|
||||||
'web-development': 'Web Development',
|
'web-development': 'Web Development',
|
||||||
@@ -130,9 +192,13 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
|||||||
'other': 'Other',
|
'other': 'Other',
|
||||||
};
|
};
|
||||||
|
|
||||||
await transporter.sendMail({
|
const fromAddress = smtpFrom || smtpUser;
|
||||||
from: `"WorkRoot Contact Form" <${smtpUser}>`,
|
|
||||||
|
// 1) Admin notification email with BCC to SMTP_USER for delivery confirmation
|
||||||
|
const adminInfo = await smtpTransporter.sendMail({
|
||||||
|
from: `"CompanySite Contact Form" <${fromAddress}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
|
bcc: process.env.SMTP_USER, // BCC to admin inbox as delivery confirmation
|
||||||
replyTo: data.email,
|
replyTo: data.email,
|
||||||
subject: `[Contact Form] ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
|
subject: `[Contact Form] ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
|
||||||
text: [
|
text: [
|
||||||
@@ -161,6 +227,47 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
|||||||
<p style="color:#888;font-size:12px;margin-top:16px">Submitted at: ${new Date().toISOString()}</p>
|
<p style="color:#888;font-size:12px;margin-top:16px">Submitted at: ${new Date().toISOString()}</p>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2) User confirmation email — proves delivery works AND provides good UX
|
||||||
|
const userConfirmInfo = await smtpTransporter.sendMail({
|
||||||
|
from: `"CompanySite" <${fromAddress}>`,
|
||||||
|
to: data.email,
|
||||||
|
subject: 'We received your message - CompanySite',
|
||||||
|
text: [
|
||||||
|
`Hi ${data.name},`,
|
||||||
|
``,
|
||||||
|
`Thank you for reaching out to us! We have received your message regarding "${subjectLabels[data.subject] ?? data.subject}".`,
|
||||||
|
``,
|
||||||
|
`Our team will review your inquiry and get back to you as soon as possible, typically within 1-2 business days.`,
|
||||||
|
``,
|
||||||
|
`For your reference, here is a summary of your submission:`,
|
||||||
|
` Subject: ${subjectLabels[data.subject] ?? data.subject}`,
|
||||||
|
` Date: ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`,
|
||||||
|
``,
|
||||||
|
`If you have any urgent questions, feel free to reply to this email.`,
|
||||||
|
``,
|
||||||
|
`Best regards,`,
|
||||||
|
`The CompanySite Team`,
|
||||||
|
].join('\n'),
|
||||||
|
html: `
|
||||||
|
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px">
|
||||||
|
<h2 style="color:#333">Thank you for contacting us!</h2>
|
||||||
|
<p>Hi ${escapeHtml(data.name)},</p>
|
||||||
|
<p>We have received your message regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.</p>
|
||||||
|
<p>Our team will review your inquiry and get back to you as soon as possible, typically within <strong>1-2 business days</strong>.</p>
|
||||||
|
<div style="background:#f9f9f9;border-left:4px solid #007bff;padding:12px 16px;margin:20px 0">
|
||||||
|
<p style="margin:4px 0"><strong>Subject:</strong> ${escapeHtml(subjectLabels[data.subject] ?? data.subject)}</p>
|
||||||
|
<p style="margin:4px 0"><strong>Date:</strong> ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
||||||
|
</div>
|
||||||
|
<p>If you have any urgent questions, feel free to reply to this email.</p>
|
||||||
|
<p style="margin-top:24px">Best regards,<br><strong>The CompanySite Team</strong></p>
|
||||||
|
<hr style="border:none;border-top:1px solid #eee;margin-top:24px">
|
||||||
|
<p style="color:#888;font-size:12px">This is an automated confirmation. Please do not reply unless you need further assistance.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { adminInfo, userConfirmInfo };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If nodemailer isn't installed, treat as graceful degradation (log only)
|
// If nodemailer isn't installed, treat as graceful degradation (log only)
|
||||||
if (err instanceof Error && err.message.includes('Cannot find module')) {
|
if (err instanceof Error && err.message.includes('Cannot find module')) {
|
||||||
@@ -302,26 +409,56 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send email
|
// Fire-and-forget: send email in background so the user gets an instant response.
|
||||||
try {
|
// Errors are logged/reported but never block the HTTP reply.
|
||||||
await sendContactEmail(formData);
|
const recipientEmail = process.env.CONTACT_EMAIL_TO || 'unknown';
|
||||||
} catch {
|
console.log('Sending contact email to: ' + recipientEmail);
|
||||||
logApiRequest({
|
|
||||||
scope: 'api.contact',
|
sendContactEmail(formData)
|
||||||
method: 'POST',
|
.then((result) => {
|
||||||
path: '/api/contact',
|
console.log('Contact emails delivered successfully:', {
|
||||||
status: 500,
|
adminRecipient: recipientEmail,
|
||||||
ip,
|
adminMessageId: result?.adminInfo?.messageId,
|
||||||
durationMs: Date.now() - startTime,
|
userRecipient: formData.email,
|
||||||
|
userMessageId: result?.userConfirmInfo?.messageId,
|
||||||
|
});
|
||||||
|
logger.info('api.contact', 'Contact emails delivered successfully', {
|
||||||
|
adminRecipient: recipientEmail,
|
||||||
|
adminMessageId: result?.adminInfo?.messageId,
|
||||||
|
userRecipient: formData.email,
|
||||||
|
userMessageId: result?.userConfirmInfo?.messageId,
|
||||||
|
subject: formData.subject,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(async (err: any) => {
|
||||||
|
console.error('Background email delivery failed:', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
responseCode: err?.responseCode,
|
||||||
|
recipient: recipientEmail,
|
||||||
|
email: formData.email,
|
||||||
|
subject: formData.subject,
|
||||||
|
});
|
||||||
|
logger.error('api.contact', 'Background email delivery failed', {
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message,
|
||||||
|
response: err?.response,
|
||||||
|
responseCode: err?.responseCode,
|
||||||
|
email: formData.email,
|
||||||
|
subject: formData.subject,
|
||||||
|
});
|
||||||
|
await captureException(err, {
|
||||||
|
scope: 'api.contact.background',
|
||||||
|
extra: {
|
||||||
|
email: formData.email,
|
||||||
|
subject: formData.subject,
|
||||||
|
smtpCode: err?.code,
|
||||||
|
smtpResponse: err?.response,
|
||||||
|
smtpResponseCode: err?.responseCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: 'Failed to send message. Please try again or email us directly.',
|
|
||||||
}),
|
|
||||||
{ status: 500, headers: rateLimitHeaders }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logApiRequest({
|
logApiRequest({
|
||||||
scope: 'api.contact',
|
scope: 'api.contact',
|
||||||
@@ -334,7 +471,7 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
|||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Message sent successfully! We'll get back to you within 24 hours.",
|
message: 'Your message has been received. We will get back to you soon.',
|
||||||
}),
|
}),
|
||||||
{ status: 200, headers: rateLimitHeaders }
|
{ status: 200, headers: rateLimitHeaders }
|
||||||
);
|
);
|
||||||
|
|||||||
+24
-4
@@ -437,7 +437,7 @@ const trustStats = [
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Message Sent!</h3>
|
<h3 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Message Sent!</h3>
|
||||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6">Thank you for reaching out. We'll get back to you within 24 hours.</p>
|
<p class="text-secondary-600 dark:text-secondary-400 mb-6">Message received! We will get back to you shortly.</p>
|
||||||
<button
|
<button
|
||||||
id="send-another"
|
id="send-another"
|
||||||
class="inline-flex items-center gap-2 text-primary hover:text-primary-700 font-medium text-sm transition-colors"
|
class="inline-flex items-center gap-2 text-primary hover:text-primary-700 font-medium text-sm transition-colors"
|
||||||
@@ -1022,10 +1022,18 @@ const trustStats = [
|
|||||||
let success = false;
|
let success = false;
|
||||||
let errorMessage = 'Something went wrong. Please try again.';
|
let errorMessage = 'Something went wrong. Please try again.';
|
||||||
|
|
||||||
|
// Track when loading started for minimum display time
|
||||||
|
const loadingStart = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Set up a 10-second timeout using AbortController
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10_000);
|
||||||
|
|
||||||
const response = await fetch('/api/contact', {
|
const response = await fetch('/api/contact', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
signal: controller.signal,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
|
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
|
||||||
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
|
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
|
||||||
@@ -1036,6 +1044,8 @@ const trustStats = [
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
const data = await response.json() as { success: boolean; message?: string; error?: string };
|
const data = await response.json() as { success: boolean; message?: string; error?: string };
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
if (response.ok && data.success) {
|
||||||
@@ -1043,8 +1053,18 @@ const trustStats = [
|
|||||||
} else {
|
} else {
|
||||||
errorMessage = data.error ?? 'Failed to send message. Please try again.';
|
errorMessage = data.error ?? 'Failed to send message. Please try again.';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
errorMessage = 'Network error. Please check your connection and try again.';
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
|
errorMessage = 'Request timed out. Please check your connection and try again.';
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Network error. Please check your connection and try again.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure loading state is shown for at least 300ms to avoid jarring flash
|
||||||
|
const elapsed = Date.now() - loadingStart;
|
||||||
|
if (elapsed < 300) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300 - elapsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track analytics
|
// Track analytics
|
||||||
@@ -1062,7 +1082,7 @@ const trustStats = [
|
|||||||
// Show success state inline
|
// Show success state inline
|
||||||
form.classList.add('hidden');
|
form.classList.add('hidden');
|
||||||
if (successState) successState.classList.remove('hidden');
|
if (successState) successState.classList.remove('hidden');
|
||||||
showToast("Message sent! We'll get back to you within 24 hours.", 'success');
|
showToast('Message received! We will get back to you shortly.', 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast(errorMessage, 'error');
|
showToast(errorMessage, 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user