Add contact form submission endpoint with database storage
This commit is contained in:
parent
f95821c08d
commit
3de9e7a7d9
@ -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
176
src/routes/contact.js
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user