Add comprehensive feature request admin functionality
- Update data model to include status, adminReply, and updatedAt fields - Hide user emails from public API responses for privacy - Add admin-only endpoints: list, reply, update status, delete - Create admin-feature-requests.html with full management UI - Add status badges and admin replies to public feature requests page - Add Feature Requests link to all admin page sidebars Admin capabilities: - View all feature requests with author emails (admin only) - Reply to feature requests with admin responses visible to public - Update status: pending, planned, in-progress, completed, declined - Delete feature requests - Filter and sort by status, votes, date
This commit is contained in:
121
chat/server.js
121
chat/server.js
@@ -11217,7 +11217,6 @@ async function handleAdminWithdrawalUpdate(req, res) {
|
||||
async function handleFeatureRequestsList(req, res) {
|
||||
const session = getUserSession(req);
|
||||
const userId = session?.userId || '';
|
||||
const userEmail = userId ? (findUserById(userId)?.email || '') : '';
|
||||
|
||||
const sorted = [...featureRequestsDb].sort((a, b) => {
|
||||
if (b.votes !== a.votes) return b.votes - a.votes;
|
||||
@@ -11230,7 +11229,8 @@ async function handleFeatureRequestsList(req, res) {
|
||||
description: fr.description,
|
||||
votes: fr.votes,
|
||||
createdAt: fr.createdAt,
|
||||
authorEmail: fr.authorEmail,
|
||||
status: fr.status || 'pending',
|
||||
adminReply: fr.adminReply || '',
|
||||
hasVoted: userId ? fr.upvoters.includes(userId) : false,
|
||||
}));
|
||||
|
||||
@@ -11264,7 +11264,10 @@ async function handleFeatureRequestCreate(req, res) {
|
||||
upvoters: [session.userId],
|
||||
authorEmail: user.email || '',
|
||||
authorId: session.userId,
|
||||
status: 'pending',
|
||||
adminReply: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
featureRequestsDb.push(featureRequest);
|
||||
@@ -11279,7 +11282,8 @@ async function handleFeatureRequestCreate(req, res) {
|
||||
description: featureRequest.description,
|
||||
votes: featureRequest.votes,
|
||||
createdAt: featureRequest.createdAt,
|
||||
authorEmail: featureRequest.authorEmail,
|
||||
status: featureRequest.status,
|
||||
adminReply: featureRequest.adminReply,
|
||||
hasVoted: true,
|
||||
}
|
||||
});
|
||||
@@ -11315,6 +11319,109 @@ async function handleFeatureRequestUpvote(req, res, id) {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAdminFeatureRequestsList(req, res) {
|
||||
const session = getAdminSession(req);
|
||||
if (!session) {
|
||||
return sendJson(res, 403, { error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const sorted = [...featureRequestsDb].sort((a, b) => {
|
||||
if (b.votes !== a.votes) return b.votes - a.votes;
|
||||
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||
});
|
||||
|
||||
const result = sorted.map(fr => ({
|
||||
id: fr.id,
|
||||
title: fr.title,
|
||||
description: fr.description,
|
||||
votes: fr.votes,
|
||||
createdAt: fr.createdAt,
|
||||
updatedAt: fr.updatedAt,
|
||||
authorEmail: fr.authorEmail,
|
||||
authorId: fr.authorId,
|
||||
status: fr.status || 'pending',
|
||||
adminReply: fr.adminReply || '',
|
||||
upvoters: fr.upvoters || [],
|
||||
}));
|
||||
|
||||
sendJson(res, 200, { featureRequests: result });
|
||||
}
|
||||
|
||||
async function handleFeatureRequestReply(req, res, id) {
|
||||
const session = getAdminSession(req);
|
||||
if (!session) {
|
||||
return sendJson(res, 403, { error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const featureRequest = featureRequestsDb.find(fr => fr.id === id);
|
||||
if (!featureRequest) {
|
||||
return sendJson(res, 404, { error: 'Feature request not found' });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await parseJsonBody(req);
|
||||
const reply = (body.reply || '').toString().trim();
|
||||
|
||||
featureRequest.adminReply = reply;
|
||||
featureRequest.updatedAt = new Date().toISOString();
|
||||
await persistFeatureRequestsDb();
|
||||
|
||||
log('Feature request reply added', { id, adminId: session.userId });
|
||||
sendJson(res, 200, { ok: true, adminReply: reply });
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error.message || 'Unable to add reply' });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFeatureRequestUpdateStatus(req, res, id) {
|
||||
const session = getAdminSession(req);
|
||||
if (!session) {
|
||||
return sendJson(res, 403, { error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const featureRequest = featureRequestsDb.find(fr => fr.id === id);
|
||||
if (!featureRequest) {
|
||||
return sendJson(res, 404, { error: 'Feature request not found' });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await parseJsonBody(req);
|
||||
const status = (body.status || '').toString().trim();
|
||||
const validStatuses = ['pending', 'planned', 'in-progress', 'completed', 'declined'];
|
||||
|
||||
if (!validStatuses.includes(status)) {
|
||||
return sendJson(res, 400, { error: 'Invalid status. Must be one of: ' + validStatuses.join(', ') });
|
||||
}
|
||||
|
||||
featureRequest.status = status;
|
||||
featureRequest.updatedAt = new Date().toISOString();
|
||||
await persistFeatureRequestsDb();
|
||||
|
||||
log('Feature request status updated', { id, status, adminId: session.userId });
|
||||
sendJson(res, 200, { ok: true, status });
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error.message || 'Unable to update status' });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFeatureRequestDelete(req, res, id) {
|
||||
const session = getAdminSession(req);
|
||||
if (!session) {
|
||||
return sendJson(res, 403, { error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const index = featureRequestsDb.findIndex(fr => fr.id === id);
|
||||
if (index === -1) {
|
||||
return sendJson(res, 404, { error: 'Feature request not found' });
|
||||
}
|
||||
|
||||
featureRequestsDb.splice(index, 1);
|
||||
await persistFeatureRequestsDb();
|
||||
|
||||
log('Feature request deleted', { id, adminId: session.userId });
|
||||
sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
async function handleContactMessagesList(req, res) {
|
||||
const session = getAdminSession(req);
|
||||
|
||||
@@ -17369,6 +17476,14 @@ async function routeInternal(req, res, url, pathname) {
|
||||
if (req.method === 'POST' && pathname === '/api/feature-requests') return handleFeatureRequestCreate(req, res);
|
||||
const featureUpvoteMatch = pathname.match(/^\/api\/feature-requests\/([a-f0-9\-]+)\/upvote$/i);
|
||||
if (req.method === 'POST' && featureUpvoteMatch) return handleFeatureRequestUpvote(req, res, featureUpvoteMatch[1]);
|
||||
// Admin feature request endpoints
|
||||
if (req.method === 'GET' && pathname === '/api/admin/feature-requests') return handleAdminFeatureRequestsList(req, res);
|
||||
const featureRequestReplyMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/reply$/i);
|
||||
if (req.method === 'POST' && featureRequestReplyMatch) return handleFeatureRequestReply(req, res, featureRequestReplyMatch[1]);
|
||||
const featureRequestStatusMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/status$/i);
|
||||
if (req.method === 'POST' && featureRequestStatusMatch) return handleFeatureRequestUpdateStatus(req, res, featureRequestStatusMatch[1]);
|
||||
const featureRequestDeleteMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)$/i);
|
||||
if (req.method === 'DELETE' && featureRequestDeleteMatch) return handleFeatureRequestDelete(req, res, featureRequestDeleteMatch[1]);
|
||||
if (req.method === 'POST' && pathname === '/api/contact') return handleContactMessageCreate(req, res);
|
||||
const contactMessagesMatch = pathname.match(/^\/api\/contact\/messages$/i);
|
||||
if (req.method === 'GET' && contactMessagesMatch) return handleContactMessagesList(req, res);
|
||||
|
||||
Reference in New Issue
Block a user