import express from 'express'
import nodemailer from 'nodemailer'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
app.use(express.json())
// Serve static files from Vite build (dist/ is at project root, one level up from dist-server/)
app.use(express.static(path.join(__dirname, '..', 'dist')))
// Contact API endpoint
app.post('/api/contact', async (req, res) => {
const { name, organisation, email, subject, message } = req.body
if (!name || !email || !subject || !message) {
return res.status(400).json({ success: false, message: 'All fields are required' })
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
return res.status(400).json({ success: false, message: 'Invalid email address' })
}
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
const contactEmail = process.env.CONTACT_EMAIL || 'andy@charlwood.xyz'
try {
// Admin notification
await transporter.sendMail({
from: `"${name}" <${process.env.SMTP_USER}>`,
replyTo: email,
to: contactEmail,
subject: `Portfolio Referral: ${subject}`,
html: `
New Patient Referral
Referring Clinician: ${name}
Organisation: ${organisation || 'Not specified'}
Email: ${email}
Subject: ${subject}
Clinical Details:
${message}
This message was sent from your portfolio contact form.
`,
})
// Auto-reply
await transporter.sendMail({
from: `"Andy Charlwood" <${process.env.SMTP_USER}>`,
to: email,
subject: 'Thanks for getting in touch!',
html: `
Thanks for your message, ${name}!
I've received your referral and will get back to you as soon as possible.
Best regards,
Andy Charlwood
Informatics Pharmacist · NHS Norfolk & Waveney ICB
This is an automated confirmation. Please do not reply to this email.
`,
})
return res.status(200).json({ success: true, message: 'Referral sent successfully!' })
} catch (error) {
console.error('Email error:', error)
return res.status(500).json({ success: false, message: 'Failed to send referral. Please try again.' })
}
})
// Chat proxy endpoint — keeps API key server-side
app.post('/api/chat', async (req, res) => {
const apiKey = process.env.OPEN_ROUTER_API_KEY
if (!apiKey) {
return res.status(500).json({ error: 'LLM API key not configured' })
}
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'HTTP-Referer': req.headers.origin || req.headers.referer || '',
'X-Title': 'Andy Charlwood Portfolio',
},
body: JSON.stringify(req.body),
})
if (!response.ok) {
return res.status(response.status).json({ error: `LLM API error: ${response.status}` })
}
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
const reader = response.body?.getReader()
if (!reader) {
return res.status(500).json({ error: 'No response body' })
}
const pump = async () => {
while (true) {
const { done, value } = await reader.read()
if (done) break
res.write(value)
}
res.end()
}
await pump()
} catch (error) {
console.error('Chat proxy error:', error)
if (!res.headersSent) {
return res.status(500).json({ error: 'Failed to proxy chat request' })
}
res.end()
}
})
// SPA fallback
app.get('*', (_req, res) => {
res.sendFile(path.join(__dirname, '..', 'dist', 'index.html'))
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})