Add contact form submission endpoint with database storage

This commit is contained in:
Z User 2026-03-27 23:46:37 +00:00
parent f95821c08d
commit 3de9e7a7d9
2 changed files with 185 additions and 0 deletions

View File

@ -8,6 +8,7 @@ import { dirname, join } from 'path';
import usersRouter from './routes/users.js'; import usersRouter from './routes/users.js';
import webhooksRouter from './routes/webhooks.js'; import webhooksRouter from './routes/webhooks.js';
import authRouter from './routes/auth.js'; import authRouter from './routes/auth.js';
import contactRouter from './routes/contact.js';
import { ApiResponse } from './utils/helpers.js'; import { ApiResponse } from './utils/helpers.js';
import { validateAccessToken, ensureUserExists } from './middleware/auth.js'; import { validateAccessToken, ensureUserExists } from './middleware/auth.js';
import { initDatabase, waitForDb } from './db/database.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/stripe': 'Stripe webhook endpoint',
'POST /api/webhooks/paypal': 'PayPal webhook endpoint', 'POST /api/webhooks/paypal': 'PayPal webhook endpoint',
'POST /api/webhooks/create-payment': 'Create payment record (requires Auth0 token)' '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 // Mount routers
app.use('/api/users', usersRouter); app.use('/api/users', usersRouter);
app.use('/api/webhooks', webhooksRouter); app.use('/api/webhooks', webhooksRouter);
app.use('/api/contact', contactRouter);
// ============================================ // ============================================
// Error Handling // Error Handling

176
src/routes/contact.js Normal file
View File

@ -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;