handling closing posts
This commit is contained in:
@@ -170,6 +170,12 @@ router.get('/posts/:id', optionalAuth, async (req, res) => {
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'role']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'closer',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'role'],
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
@@ -202,8 +208,8 @@ router.get('/posts/:id', optionalAuth, async (req, res) => {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
await post.increment('viewCount');
|
||||
// Increment view count without updating the updatedAt timestamp
|
||||
await post.increment('viewCount', { silent: true });
|
||||
|
||||
// Build nested comment tree
|
||||
const isAdmin = req.user && req.user.role === 'admin';
|
||||
@@ -424,7 +430,94 @@ router.patch('/posts/:id/status', authenticateToken, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid status value' });
|
||||
}
|
||||
|
||||
await post.update({ status });
|
||||
// Set closedBy and closedAt when closing, clear them when reopening
|
||||
const updateData = {
|
||||
status,
|
||||
closedBy: status === 'closed' ? req.user.id : null,
|
||||
closedAt: status === 'closed' ? new Date() : null
|
||||
};
|
||||
|
||||
await post.update(updateData);
|
||||
|
||||
// Send email notifications when closing (not when reopening)
|
||||
if (status === 'closed') {
|
||||
(async () => {
|
||||
try {
|
||||
const closerUser = await User.findByPk(req.user.id, {
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
});
|
||||
|
||||
const postWithAuthor = await ForumPost.findByPk(post.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Get all unique participants (author + commenters)
|
||||
const comments = await ForumComment.findAll({
|
||||
where: {
|
||||
postId: post.id,
|
||||
isDeleted: false
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'email', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Collect unique recipients (excluding the closer)
|
||||
const recipientMap = new Map();
|
||||
|
||||
// Add post author if not the closer
|
||||
if (postWithAuthor.author && postWithAuthor.author.id !== req.user.id) {
|
||||
recipientMap.set(postWithAuthor.author.id, postWithAuthor.author);
|
||||
}
|
||||
|
||||
// Add all comment authors (excluding closer)
|
||||
comments.forEach(comment => {
|
||||
if (comment.author && comment.author.id !== req.user.id && !recipientMap.has(comment.author.id)) {
|
||||
recipientMap.set(comment.author.id, comment.author);
|
||||
}
|
||||
});
|
||||
|
||||
// Send emails to all recipients
|
||||
const recipients = Array.from(recipientMap.values());
|
||||
const emailPromises = recipients.map(recipient =>
|
||||
emailServices.forum.sendForumPostClosedNotification(
|
||||
recipient,
|
||||
closerUser,
|
||||
postWithAuthor,
|
||||
updateData.closedAt
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.allSettled(emailPromises);
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Post closure notification emails sent", {
|
||||
postId: post.id,
|
||||
recipientCount: recipients.length,
|
||||
closedBy: req.user.id
|
||||
});
|
||||
} catch (emailError) {
|
||||
// Email errors don't block the closure
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Failed to send post closure notification emails", {
|
||||
error: emailError.message,
|
||||
stack: emailError.stack,
|
||||
postId: req.params.id
|
||||
});
|
||||
console.error("Email notification error:", emailError);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
const updatedPost = await ForumPost.findByPk(post.id, {
|
||||
include: [
|
||||
@@ -433,6 +526,12 @@ router.patch('/posts/:id/status', authenticateToken, async (req, res) => {
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'closer',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName'],
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: PostTag,
|
||||
as: 'tags',
|
||||
@@ -496,13 +595,15 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
|
||||
}
|
||||
}
|
||||
|
||||
// Update the post with accepted answer
|
||||
// Update the post with accepted answer and close the discussion
|
||||
await post.update({
|
||||
acceptedAnswerId: commentId || null,
|
||||
status: commentId ? 'answered' : 'open'
|
||||
status: commentId ? 'closed' : 'open',
|
||||
closedBy: commentId ? req.user.id : null,
|
||||
closedAt: commentId ? new Date() : null
|
||||
});
|
||||
|
||||
// Send email notification if marking an answer (not unmarking)
|
||||
// Send email notifications if marking an answer (not unmarking)
|
||||
if (commentId) {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -520,7 +621,17 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
});
|
||||
|
||||
// Only send email if not marking your own comment as answer
|
||||
const postWithAuthor = await ForumPost.findByPk(post.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Send answer accepted email if not marking your own comment as answer
|
||||
if (comment && comment.authorId !== req.user.id) {
|
||||
await emailServices.forum.sendForumAnswerAcceptedNotification(
|
||||
comment.author,
|
||||
@@ -529,9 +640,55 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
|
||||
comment
|
||||
);
|
||||
}
|
||||
|
||||
// Send post closure notifications to all participants
|
||||
// Get all unique participants (author + commenters)
|
||||
const comments = await ForumComment.findAll({
|
||||
where: {
|
||||
postId: post.id,
|
||||
isDeleted: false
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'email', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Collect unique recipients (excluding the post author who marked the answer)
|
||||
const recipientMap = new Map();
|
||||
|
||||
// Don't send to post author since they closed it
|
||||
// Add all comment authors (excluding post author)
|
||||
comments.forEach(c => {
|
||||
if (c.author && c.author.id !== req.user.id && !recipientMap.has(c.author.id)) {
|
||||
recipientMap.set(c.author.id, c.author);
|
||||
}
|
||||
});
|
||||
|
||||
// Send closure notification emails to all recipients
|
||||
const recipients = Array.from(recipientMap.values());
|
||||
const closureEmailPromises = recipients.map(recipient =>
|
||||
emailServices.forum.sendForumPostClosedNotification(
|
||||
recipient,
|
||||
postAuthor,
|
||||
postWithAuthor,
|
||||
new Date()
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.allSettled(closureEmailPromises);
|
||||
|
||||
logger.info("Answer marked and closure notifications sent", {
|
||||
postId: post.id,
|
||||
commentId: commentId,
|
||||
closureNotificationCount: recipients.length
|
||||
});
|
||||
} catch (emailError) {
|
||||
// Email errors don't block answer marking
|
||||
logger.error("Failed to send answer accepted notification email", {
|
||||
logger.error("Failed to send notification emails", {
|
||||
error: emailError.message,
|
||||
stack: emailError.stack,
|
||||
commentId: commentId,
|
||||
@@ -587,6 +744,11 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
// Prevent comments on closed posts
|
||||
if (post.status === 'closed') {
|
||||
return res.status(403).json({ error: 'Cannot comment on a closed discussion' });
|
||||
}
|
||||
|
||||
// Validate parent comment if provided
|
||||
if (parentCommentId) {
|
||||
const parentComment = await ForumComment.findByPk(parentCommentId);
|
||||
@@ -606,8 +768,9 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
||||
images
|
||||
});
|
||||
|
||||
// Increment comment count
|
||||
// Increment comment count and update post's updatedAt to reflect activity
|
||||
await post.increment('commentCount');
|
||||
await post.update({ updatedAt: new Date() });
|
||||
|
||||
const commentWithDetails = await ForumComment.findByPk(comment.id, {
|
||||
include: [
|
||||
@@ -1073,4 +1236,159 @@ router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, asy
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/forum/admin/posts/:id/close - Admin close discussion
|
||||
router.patch('/admin/posts/:id/close', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const post = await ForumPost.findByPk(req.params.id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
if (post.status === 'closed') {
|
||||
return res.status(400).json({ error: 'Post is already closed' });
|
||||
}
|
||||
|
||||
const closedAt = new Date();
|
||||
|
||||
// Close the post
|
||||
await post.update({
|
||||
status: 'closed',
|
||||
closedBy: req.user.id,
|
||||
closedAt: closedAt
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Admin closed post", {
|
||||
postId: req.params.id,
|
||||
adminId: req.user.id,
|
||||
originalAuthorId: post.authorId
|
||||
});
|
||||
|
||||
// Send email notifications asynchronously
|
||||
(async () => {
|
||||
try {
|
||||
const admin = await User.findByPk(req.user.id, {
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
});
|
||||
|
||||
// Get all unique participants (author + commenters)
|
||||
const comments = await ForumComment.findAll({
|
||||
where: {
|
||||
postId: post.id,
|
||||
isDeleted: false
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'email', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Collect unique recipients (author + all comment authors)
|
||||
const recipientMap = new Map();
|
||||
|
||||
// Add post author if not the admin
|
||||
if (post.author && post.author.id !== req.user.id) {
|
||||
recipientMap.set(post.author.id, post.author);
|
||||
}
|
||||
|
||||
// Add all comment authors (excluding admin)
|
||||
comments.forEach(comment => {
|
||||
if (comment.author && comment.author.id !== req.user.id && !recipientMap.has(comment.author.id)) {
|
||||
recipientMap.set(comment.author.id, comment.author);
|
||||
}
|
||||
});
|
||||
|
||||
// Send emails to all recipients
|
||||
const recipients = Array.from(recipientMap.values());
|
||||
const emailPromises = recipients.map(recipient =>
|
||||
emailServices.forum.sendForumPostClosedNotification(
|
||||
recipient,
|
||||
admin,
|
||||
post,
|
||||
closedAt
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.allSettled(emailPromises);
|
||||
|
||||
reqLogger.info("Post closure notification emails sent", {
|
||||
postId: post.id,
|
||||
recipientCount: recipients.length
|
||||
});
|
||||
} catch (emailError) {
|
||||
// Email errors don't block the closure
|
||||
reqLogger.error("Failed to send post closure notification emails", {
|
||||
error: emailError.message,
|
||||
stack: emailError.stack,
|
||||
postId: req.params.id
|
||||
});
|
||||
console.error("Email notification error:", emailError);
|
||||
}
|
||||
})();
|
||||
|
||||
res.status(200).json({ message: 'Post closed successfully', post });
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Admin post closure failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
postId: req.params.id,
|
||||
adminId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/forum/admin/posts/:id/reopen - Admin reopen discussion
|
||||
router.patch('/admin/posts/:id/reopen', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
if (post.status !== 'closed') {
|
||||
return res.status(400).json({ error: 'Post is not closed' });
|
||||
}
|
||||
|
||||
// Reopen the post
|
||||
await post.update({
|
||||
status: 'open',
|
||||
closedBy: null,
|
||||
closedAt: null
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Admin reopened post", {
|
||||
postId: req.params.id,
|
||||
adminId: req.user.id,
|
||||
originalAuthorId: post.authorId
|
||||
});
|
||||
|
||||
res.status(200).json({ message: 'Post reopened successfully', post });
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Admin post reopen failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
postId: req.params.id,
|
||||
adminId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user