feat(contact): add contact form with admin panel and navbar dropdown
Database changes: - Added ContactMessage model to Prisma schema - Fields: userId, username, firstName, lastName, email, subject, message, status, ipAddress - Status enum: new, read, resolved - Relation to User model Backend changes: - Added POST /api/public/contact endpoint for form submissions - Works for both authenticated and non-authenticated users - Validation for email, subject (3-255 chars), message (10-5000 chars) - Activity logging for submissions - Added admin endpoints: - GET /api/admin/contact-messages - list with filtering by status - GET /api/admin/contact-messages/:id - view single message (auto-marks as read) - PATCH /api/admin/contact-messages/:id/status - update status - DELETE /api/admin/contact-messages/:id - delete message Frontend changes: - Created ContactPage at /contact route - For non-logged-in users: firstName, lastName, email, subject, message fields - For logged-in users: auto-fills username, shows only email, subject, message - Character counter for message (max 5000) - Success screen with auto-redirect to homepage - Created ContactMessagesPage at /admin/contact-messages - Two-column layout: message list + detail view - Filter by status (all, new, read, resolved) - View message details with sender info and IP address - Update status and delete messages - Added admin dropdown menu to Navbar - Desktop: dropdown with Activity Logs and Contact Messages - Mobile: expandable submenu - Click outside to close on desktop - ChevronDown icon rotates when open Note: CAPTCHA integration planned for future enhancement
This commit is contained in:
@@ -332,3 +332,167 @@ router.get('/activity-logs/stats', authenticate, requireAdmin, async (req, res,
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/admin/contact-messages - Get all contact messages with filtering
|
||||
router.get('/contact-messages', authenticate, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { status, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
const where = {};
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const [messages, total] = await Promise.all([
|
||||
prisma.contactMessage.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: parseInt(limit),
|
||||
skip: parseInt(offset),
|
||||
}),
|
||||
prisma.contactMessage.count({ where }),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
messages,
|
||||
total,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/admin/contact-messages/:id - Get single contact message
|
||||
router.get('/contact-messages/:id', authenticate, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const message = await prisma.contactMessage.findUnique({
|
||||
where: { id: parseInt(id) },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Contact message not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Mark as read if it's new
|
||||
if (message.status === 'new') {
|
||||
await prisma.contactMessage.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: { status: 'read' },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: message,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/admin/contact-messages/:id/status - Update contact message status
|
||||
router.patch('/contact-messages/:id/status', authenticate, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!['new', 'read', 'resolved'].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid status. Must be one of: new, read, resolved',
|
||||
});
|
||||
}
|
||||
|
||||
const message = await prisma.contactMessage.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: { status },
|
||||
});
|
||||
|
||||
// Log the action
|
||||
activityLog({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
ipAddress: getClientIP(req),
|
||||
action: 'contact.update_status',
|
||||
category: 'admin',
|
||||
resource: `contact:${id}`,
|
||||
method: 'PATCH',
|
||||
path: `/api/admin/contact-messages/${id}/status`,
|
||||
metadata: { status },
|
||||
success: true,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: message,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/admin/contact-messages/:id - Delete contact message
|
||||
router.delete('/contact-messages/:id', authenticate, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await prisma.contactMessage.delete({
|
||||
where: { id: parseInt(id) },
|
||||
});
|
||||
|
||||
// Log the action
|
||||
activityLog({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
ipAddress: getClientIP(req),
|
||||
action: 'contact.delete',
|
||||
category: 'admin',
|
||||
resource: `contact:${id}`,
|
||||
method: 'DELETE',
|
||||
path: `/api/admin/contact-messages/${id}`,
|
||||
metadata: {},
|
||||
success: true,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Contact message deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user