From 3de9e7a7d91523121bd94ce448cd3c10cc846431 Mon Sep 17 00:00:00 2001 From: Z User Date: Fri, 27 Mar 2026 23:46:37 +0000 Subject: [PATCH] Add contact form submission endpoint with database storage --- src/index.js | 9 +++ src/routes/contact.js | 176 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 src/routes/contact.js diff --git a/src/index.js b/src/index.js index 20ea6d1..d3f3feb 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,7 @@ import { dirname, join } from 'path'; import usersRouter from './routes/users.js'; import webhooksRouter from './routes/webhooks.js'; import authRouter from './routes/auth.js'; +import contactRouter from './routes/contact.js'; import { ApiResponse } from './utils/helpers.js'; import { validateAccessToken, ensureUserExists } from './middleware/auth.js'; import { initDatabase, waitForDb } from './db/database.js'; @@ -104,6 +105,13 @@ app.get('/api', (req, res) => { 'POST /api/webhooks/stripe': 'Stripe webhook endpoint', 'POST /api/webhooks/paypal': 'PayPal webhook endpoint', 'POST /api/webhooks/create-payment': 'Create payment record (requires Auth0 token)' + }, + contact: { + 'POST /api/contact': 'Submit contact form (public)', + 'GET /api/contact': 'List contact submissions (admin only)', + 'GET /api/contact/:id': 'Get contact submission (admin only)', + 'PUT /api/contact/:id/status': 'Update submission status (admin only)', + 'DELETE /api/contact/:id': 'Delete submission (admin only)' } } })); @@ -126,6 +134,7 @@ app.get('/api/logout', (req, res) => res.redirect('/api/auth/logout')); // Mount routers app.use('/api/users', usersRouter); app.use('/api/webhooks', webhooksRouter); +app.use('/api/contact', contactRouter); // ============================================ // Error Handling diff --git a/src/routes/contact.js b/src/routes/contact.js new file mode 100644 index 0000000..0815543 --- /dev/null +++ b/src/routes/contact.js @@ -0,0 +1,176 @@ +import express from 'express'; +import { dbRun, dbAll, dbGet } from '../db/database.js'; +import { generateId, ApiResponse, asyncHandler } from '../utils/helpers.js'; + +const router = express.Router(); + +/** + * @route POST /api/contact + * @desc Submit a contact form + * @access Public + */ +router.post('/', asyncHandler(async (req, res) => { + const { name, email, company, message } = req.body; + + // Validate required fields + if (!name || !name.trim()) { + return res.status(400).json(ApiResponse(false, null, 'Name is required')); + } + if (!email || !email.trim()) { + return res.status(400).json(ApiResponse(false, null, 'Email is required')); + } + if (!message || !message.trim()) { + return res.status(400).json(ApiResponse(false, null, 'Message is required')); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json(ApiResponse(false, null, 'Invalid email format')); + } + + try { + // Create contact_submissions table if not exists + await dbRun(` + CREATE TABLE IF NOT EXISTS contact_submissions ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL, + company TEXT, + message TEXT NOT NULL, + status TEXT DEFAULT 'new', + ip_address TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Insert the contact submission + const submissionId = generateId(); + await dbRun( + `INSERT INTO contact_submissions (id, name, email, company, message, ip_address, user_agent) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + submissionId, + name.trim(), + email.trim().toLowerCase(), + company?.trim() || null, + message.trim(), + req.ip || req.connection?.remoteAddress || null, + req.get('User-Agent') || null + ] + ); + + console.log(`New contact submission: ${submissionId} from ${email}`); + + res.status(201).json(ApiResponse(true, { + id: submissionId, + message: 'Thank you for your message! We\'ll get back to you soon.' + })); + + } catch (error) { + console.error('Contact submission error:', error); + res.status(500).json(ApiResponse(false, null, 'Failed to submit contact form')); + } +})); + +/** + * @route GET /api/contact + * @desc List all contact submissions (admin only) + * @access Private (Admin) + */ +router.get('/', asyncHandler(async (req, res) => { + try { + const submissions = await dbAll( + `SELECT * FROM contact_submissions ORDER BY created_at DESC` + ); + res.json(ApiResponse(true, submissions)); + } catch (error) { + console.error('Error fetching contact submissions:', error); + res.status(500).json(ApiResponse(false, null, 'Failed to fetch submissions')); + } +})); + +/** + * @route GET /api/contact/:id + * @desc Get a specific contact submission (admin only) + * @access Private (Admin) + */ +router.get('/:id', asyncHandler(async (req, res) => { + const { id } = req.params; + + try { + const submission = await dbGet( + `SELECT * FROM contact_submissions WHERE id = ?`, + [id] + ); + + if (!submission) { + return res.status(404).json(ApiResponse(false, null, 'Submission not found')); + } + + res.json(ApiResponse(true, submission)); + } catch (error) { + console.error('Error fetching contact submission:', error); + res.status(500).json(ApiResponse(false, null, 'Failed to fetch submission')); + } +})); + +/** + * @route PUT /api/contact/:id/status + * @desc Update contact submission status (admin only) + * @access Private (Admin) + */ +router.put('/:id/status', asyncHandler(async (req, res) => { + const { id } = req.params; + const { status } = req.body; + + const validStatuses = ['new', 'read', 'replied', 'archived', 'spam']; + if (!validStatuses.includes(status)) { + return res.status(400).json(ApiResponse(false, null, 'Invalid status')); + } + + try { + const result = await dbRun( + `UPDATE contact_submissions SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + [status, id] + ); + + if (result.changes === 0) { + return res.status(404).json(ApiResponse(false, null, 'Submission not found')); + } + + res.json(ApiResponse(true, { id, status })); + } catch (error) { + console.error('Error updating contact submission:', error); + res.status(500).json(ApiResponse(false, null, 'Failed to update submission')); + } +})); + +/** + * @route DELETE /api/contact/:id + * @desc Delete a contact submission (admin only) + * @access Private (Admin) + */ +router.delete('/:id', asyncHandler(async (req, res) => { + const { id } = req.params; + + try { + const result = await dbRun( + `DELETE FROM contact_submissions WHERE id = ?`, + [id] + ); + + if (result.changes === 0) { + return res.status(404).json(ApiResponse(false, null, 'Submission not found')); + } + + res.json(ApiResponse(true, { message: 'Submission deleted' })); + } catch (error) { + console.error('Error deleting contact submission:', error); + res.status(500).json(ApiResponse(false, null, 'Failed to delete submission')); + } +})); + +export default router;