From f7767dfd1311c049ac8ea60885fcefba5cf4399f Mon Sep 17 00:00:00 2001
From: jackiettran <41605212+jackiettran@users.noreply.github.com>
Date: Thu, 20 Nov 2025 18:08:30 -0500
Subject: [PATCH] deletion reason and email for soft deleted forum posts and
comments by admin
---
backend/models/ForumComment.js | 4 +
backend/models/ForumPost.js | 4 +
backend/routes/forum.js | 118 ++++++-
backend/routes/items.js | 3 +-
.../services/email/core/TemplateManager.js | 2 +
.../email/domain/ForumEmailService.js | 123 +++++++
.../emails/forumCommentDeletionToAuthor.html | 314 ++++++++++++++++++
.../emails/forumPostDeletionToAuthor.html | 305 +++++++++++++++++
frontend/src/pages/ForumPostDetail.tsx | 55 ++-
frontend/src/services/api.ts | 4 +-
10 files changed, 911 insertions(+), 21 deletions(-)
create mode 100644 backend/templates/emails/forumCommentDeletionToAuthor.html
create mode 100644 backend/templates/emails/forumPostDeletionToAuthor.html
diff --git a/backend/models/ForumComment.js b/backend/models/ForumComment.js
index 1495ae9..99a951f 100644
--- a/backend/models/ForumComment.js
+++ b/backend/models/ForumComment.js
@@ -55,6 +55,10 @@ const ForumComment = sequelize.define('ForumComment', {
deletedAt: {
type: DataTypes.DATE,
allowNull: true
+ },
+ deletionReason: {
+ type: DataTypes.TEXT,
+ allowNull: true
}
});
diff --git a/backend/models/ForumPost.js b/backend/models/ForumPost.js
index a612b89..096583d 100644
--- a/backend/models/ForumPost.js
+++ b/backend/models/ForumPost.js
@@ -85,6 +85,10 @@ const ForumPost = sequelize.define('ForumPost', {
type: DataTypes.DATE,
allowNull: true
},
+ deletionReason: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
closedBy: {
type: DataTypes.UUID,
allowNull: true,
diff --git a/backend/routes/forum.js b/backend/routes/forum.js
index 51ea2a4..f77f198 100644
--- a/backend/routes/forum.js
+++ b/backend/routes/forum.js
@@ -1250,26 +1250,68 @@ router.get('/tags', async (req, res) => {
// DELETE /api/forum/admin/posts/:id - Admin soft-delete post
router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, res) => {
try {
- const post = await ForumPost.findByPk(req.params.id);
+ const { reason } = req.body;
+
+ if (!reason || !reason.trim()) {
+ return res.status(400).json({ error: "Deletion reason is required" });
+ }
+
+ 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.isDeleted) {
+ return res.status(400).json({ error: 'Post is already deleted' });
+ }
+
// Soft delete the post
await post.update({
isDeleted: true,
deletedBy: req.user.id,
- deletedAt: new Date()
+ deletedAt: new Date(),
+ deletionReason: reason.trim()
});
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Admin deleted post", {
postId: req.params.id,
adminId: req.user.id,
- originalAuthorId: post.authorId
+ originalAuthorId: post.authorId,
+ reason: reason.trim()
});
+ // Send email notification to post author (non-blocking)
+ (async () => {
+ try {
+ const admin = await User.findByPk(req.user.id, {
+ attributes: ['id', 'username', 'firstName', 'lastName']
+ });
+
+ if (post.author && admin) {
+ await emailServices.forum.sendForumPostDeletionNotification(
+ post.author,
+ admin,
+ post,
+ reason.trim()
+ );
+ console.log(`Forum post deletion notification email sent to author ${post.authorId}`);
+ }
+ } catch (emailError) {
+ // Log but don't fail the deletion
+ console.error('Failed to send forum post deletion notification email:', emailError.message);
+ }
+ })();
+
res.status(200).json({ message: 'Post deleted successfully', post });
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
@@ -1292,11 +1334,16 @@ router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async
return res.status(404).json({ error: 'Post not found' });
}
+ if (!post.isDeleted) {
+ return res.status(400).json({ error: 'Post is not deleted' });
+ }
+
// Restore the post
await post.update({
isDeleted: false,
deletedBy: null,
- deletedAt: null
+ deletedAt: null,
+ deletionReason: null
});
const reqLogger = logger.withRequestId(req.id);
@@ -1322,25 +1369,44 @@ router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async
// DELETE /api/forum/admin/comments/:id - Admin soft-delete comment
router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req, res) => {
try {
- const comment = await ForumComment.findByPk(req.params.id);
+ const { reason } = req.body;
+
+ if (!reason || !reason.trim()) {
+ return res.status(400).json({ error: "Deletion reason is required" });
+ }
+
+ const comment = await ForumComment.findByPk(req.params.id, {
+ include: [
+ {
+ model: User,
+ as: 'author',
+ attributes: ['id', 'username', 'firstName', 'lastName', 'email']
+ }
+ ]
+ });
if (!comment) {
return res.status(404).json({ error: 'Comment not found' });
}
+ if (comment.isDeleted) {
+ return res.status(400).json({ error: 'Comment is already deleted' });
+ }
+
// Soft delete the comment (preserve original content for potential restoration)
await comment.update({
isDeleted: true,
deletedBy: req.user.id,
- deletedAt: new Date()
+ deletedAt: new Date(),
+ deletionReason: reason.trim()
});
- // Decrement comment count if not already deleted
- if (!comment.isDeleted) {
- const post = await ForumPost.findByPk(comment.postId);
- if (post && post.commentCount > 0) {
- await post.decrement('commentCount');
- }
+ // Decrement comment count
+ const post = await ForumPost.findByPk(comment.postId, {
+ attributes: ['id', 'title']
+ });
+ if (post && post.commentCount > 0) {
+ await post.decrement('commentCount');
}
const reqLogger = logger.withRequestId(req.id);
@@ -1348,9 +1414,32 @@ router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req
commentId: req.params.id,
adminId: req.user.id,
originalAuthorId: comment.authorId,
- postId: comment.postId
+ postId: comment.postId,
+ reason: reason.trim()
});
+ // Send email notification to comment author (non-blocking)
+ (async () => {
+ try {
+ const admin = await User.findByPk(req.user.id, {
+ attributes: ['id', 'username', 'firstName', 'lastName']
+ });
+
+ if (comment.author && admin && post) {
+ await emailServices.forum.sendForumCommentDeletionNotification(
+ comment.author,
+ admin,
+ post,
+ reason.trim()
+ );
+ console.log(`Forum comment deletion notification email sent to author ${comment.authorId}`);
+ }
+ } catch (emailError) {
+ // Log but don't fail the deletion
+ console.error('Failed to send forum comment deletion notification email:', emailError.message);
+ }
+ })();
+
res.status(200).json({ message: 'Comment deleted successfully', comment });
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
@@ -1381,7 +1470,8 @@ router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, asy
await comment.update({
isDeleted: false,
deletedBy: null,
- deletedAt: null
+ deletedAt: null,
+ deletionReason: null
});
// Increment comment count
diff --git a/backend/routes/items.js b/backend/routes/items.js
index bb2a1d9..32f2a98 100644
--- a/backend/routes/items.js
+++ b/backend/routes/items.js
@@ -483,7 +483,8 @@ router.patch("/admin/:id/restore", authenticateToken, requireAdmin, async (req,
await item.update({
isDeleted: false,
deletedBy: null,
- deletedAt: null
+ deletedAt: null,
+ deletionReason: null
});
const updatedItem = await Item.findByPk(item.id, {
diff --git a/backend/services/email/core/TemplateManager.js b/backend/services/email/core/TemplateManager.js
index e6a1083..d4f7ba5 100644
--- a/backend/services/email/core/TemplateManager.js
+++ b/backend/services/email/core/TemplateManager.js
@@ -65,6 +65,8 @@ class TemplateManager {
"forumThreadActivityToParticipant.html",
"forumPostClosed.html",
"forumItemRequestNotification.html",
+ "forumPostDeletionToAuthor.html",
+ "forumCommentDeletionToAuthor.html",
];
for (const templateFile of templateFiles) {
diff --git a/backend/services/email/domain/ForumEmailService.js b/backend/services/email/domain/ForumEmailService.js
index 4db01cf..0b791be 100644
--- a/backend/services/email/domain/ForumEmailService.js
+++ b/backend/services/email/domain/ForumEmailService.js
@@ -9,6 +9,7 @@ const TemplateManager = require("../core/TemplateManager");
* - Sending answer accepted notifications
* - Sending thread activity notifications to participants
* - Sending location-based item request notifications to nearby users
+ * - Sending post/comment deletion notifications to authors
*/
class ForumEmailService {
constructor() {
@@ -386,6 +387,128 @@ class ForumEmailService {
}
}
+ /**
+ * Send notification when a forum post is deleted by admin
+ * @param {Object} postAuthor - Post author user object
+ * @param {string} postAuthor.firstName - Post author's first name
+ * @param {string} postAuthor.email - Post author's email
+ * @param {Object} admin - Admin user object who deleted the post
+ * @param {string} admin.firstName - Admin's first name
+ * @param {string} admin.lastName - Admin's last name
+ * @param {Object} post - Forum post object
+ * @param {string} post.title - Post title
+ * @param {string} deletionReason - Reason for deletion
+ * @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
+ */
+ async sendForumPostDeletionNotification(postAuthor, admin, post, deletionReason) {
+ if (!this.initialized) {
+ await this.initialize();
+ }
+
+ try {
+ const supportEmail = process.env.SUPPORT_EMAIL;
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+
+ const variables = {
+ postAuthorName: postAuthor.firstName || "there",
+ adminName: `${admin.firstName} ${admin.lastName}`.trim() || "An administrator",
+ postTitle: post.title,
+ deletionReason,
+ supportEmail,
+ forumUrl: `${frontendUrl}/forum`,
+ };
+
+ const htmlContent = await this.templateManager.renderTemplate(
+ "forumPostDeletionToAuthor",
+ variables
+ );
+
+ const subject = `Important: Your forum post "${post.title}" has been removed`;
+
+ const result = await this.emailClient.sendEmail(
+ postAuthor.email,
+ subject,
+ htmlContent
+ );
+
+ if (result.success) {
+ console.log(
+ `Forum post deletion notification email sent to ${postAuthor.email}`
+ );
+ }
+
+ return result;
+ } catch (error) {
+ console.error(
+ "Failed to send forum post deletion notification email:",
+ error
+ );
+ return { success: false, error: error.message };
+ }
+ }
+
+ /**
+ * Send notification when a forum comment is deleted by admin
+ * @param {Object} commentAuthor - Comment author user object
+ * @param {string} commentAuthor.firstName - Comment author's first name
+ * @param {string} commentAuthor.email - Comment author's email
+ * @param {Object} admin - Admin user object who deleted the comment
+ * @param {string} admin.firstName - Admin's first name
+ * @param {string} admin.lastName - Admin's last name
+ * @param {Object} post - Forum post object the comment belongs to
+ * @param {string} post.title - Post title
+ * @param {string} post.id - Post ID
+ * @param {string} deletionReason - Reason for deletion
+ * @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
+ */
+ async sendForumCommentDeletionNotification(commentAuthor, admin, post, deletionReason) {
+ if (!this.initialized) {
+ await this.initialize();
+ }
+
+ try {
+ const supportEmail = process.env.SUPPORT_EMAIL;
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+ const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
+
+ const variables = {
+ commentAuthorName: commentAuthor.firstName || "there",
+ adminName: `${admin.firstName} ${admin.lastName}`.trim() || "An administrator",
+ postTitle: post.title,
+ postUrl,
+ deletionReason,
+ supportEmail,
+ };
+
+ const htmlContent = await this.templateManager.renderTemplate(
+ "forumCommentDeletionToAuthor",
+ variables
+ );
+
+ const subject = `Your comment on "${post.title}" has been removed`;
+
+ const result = await this.emailClient.sendEmail(
+ commentAuthor.email,
+ subject,
+ htmlContent
+ );
+
+ if (result.success) {
+ console.log(
+ `Forum comment deletion notification email sent to ${commentAuthor.email}`
+ );
+ }
+
+ return result;
+ } catch (error) {
+ console.error(
+ "Failed to send forum comment deletion notification email:",
+ error
+ );
+ return { success: false, error: error.message };
+ }
+ }
+
/**
* Send notification to nearby users about an item request
* @param {Object} recipient - Recipient user object
diff --git a/backend/templates/emails/forumCommentDeletionToAuthor.html b/backend/templates/emails/forumCommentDeletionToAuthor.html
new file mode 100644
index 0000000..6d09ddc
--- /dev/null
+++ b/backend/templates/emails/forumCommentDeletionToAuthor.html
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+ Your Forum Comment Has Been Removed
+
+
+
+
+
+
+
+
Hi {{commentAuthorName}},
+
+
Your Comment Has Been Removed
+
+
We're writing to inform you that your comment has been removed from a forum discussion by {{adminName}}.
+
+
+
Comment on:
+
{{postTitle}}
+
View Discussion →
+
+
+
+
Reason for Removal:
+
{{deletionReason}}
+
+
+
+
What this means:
+
+ Your comment is no longer visible to other community members
+ The comment content has been preserved in case of appeal
+ The discussion thread remains active for other participants
+ You can still participate in other forum discussions
+
+
+
+
Need Help or Have Questions?
+
If you believe this removal was made in error or if you have questions about our community guidelines, please don't hesitate to contact our support team:
+
+
+ Contact Support
+
+
+
+
Review Our Community Guidelines:
+
To ensure a positive experience for all members, please review our community guidelines. We appreciate respectful, constructive contributions that help build a supportive community.
+
+
+
Thank you for your understanding, and we look forward to your continued participation in our community.
+
+
Best regards,
+ The RentAll Team
+
+
+
+
+
+
diff --git a/backend/templates/emails/forumPostDeletionToAuthor.html b/backend/templates/emails/forumPostDeletionToAuthor.html
new file mode 100644
index 0000000..d23cc0f
--- /dev/null
+++ b/backend/templates/emails/forumPostDeletionToAuthor.html
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+ Your Forum Post Has Been Removed
+
+
+
+
+
+
+
+
Hi {{postAuthorName}},
+
+
Your Forum Post Has Been Removed
+
+
We're writing to inform you that your forum post has been removed from RentAll by {{adminName}}.
+
+
+
+
+
Reason for Removal:
+
{{deletionReason}}
+
+
+
+
What this means:
+
+ Your post is no longer visible to other community members
+ All comments on this post are also hidden
+ The post cannot receive new comments or activity
+ You may still see it in your dashboard if viewing as an admin
+
+
+
+
Need Help or Have Questions?
+
If you believe this removal was made in error or if you have questions about our community guidelines, please don't hesitate to contact our support team:
+
+
+ Contact Support
+
+
+
+
Review Our Community Guidelines:
+
To prevent future removals, please familiarize yourself with our community guidelines and forum standards. Our team is happy to help you understand how to contribute positively to the RentAll community.
+
+
+
You can continue participating in the forum by visiting our community forum .
+
+
Thank you for your understanding.
+
+
Best regards,
+ The RentAll Team
+
+
+
+
+
+
diff --git a/frontend/src/pages/ForumPostDetail.tsx b/frontend/src/pages/ForumPostDetail.tsx
index 6e05a8c..ae5b1c5 100644
--- a/frontend/src/pages/ForumPostDetail.tsx
+++ b/frontend/src/pages/ForumPostDetail.tsx
@@ -23,6 +23,7 @@ const ForumPostDetail: React.FC = () => {
type: 'deletePost' | 'deleteComment' | 'restorePost' | 'restoreComment' | 'closePost' | 'reopenPost';
id?: string;
} | null>(null);
+ const [deletionReason, setDeletionReason] = useState('');
// Read filter from URL query param
const filter = searchParams.get('filter') || 'active';
@@ -174,13 +175,19 @@ const ForumPostDetail: React.FC = () => {
const confirmAdminAction = async () => {
if (!adminAction) return;
+ // Validate deletion reason for delete actions
+ if ((adminAction.type === 'deletePost' || adminAction.type === 'deleteComment') && !deletionReason.trim()) {
+ alert('Please provide a reason for deletion');
+ return;
+ }
+
try {
setActionLoading(true);
setShowAdminModal(false);
switch (adminAction.type) {
case 'deletePost':
- await forumAPI.adminDeletePost(id!);
+ await forumAPI.adminDeletePost(id!, deletionReason.trim());
break;
case 'restorePost':
await forumAPI.adminRestorePost(id!);
@@ -192,7 +199,7 @@ const ForumPostDetail: React.FC = () => {
await forumAPI.adminReopenPost(id!);
break;
case 'deleteComment':
- await forumAPI.adminDeleteComment(adminAction.id!);
+ await forumAPI.adminDeleteComment(adminAction.id!, deletionReason.trim());
break;
case 'restoreComment':
await forumAPI.adminRestoreComment(adminAction.id!);
@@ -205,12 +212,14 @@ const ForumPostDetail: React.FC = () => {
} finally {
setActionLoading(false);
setAdminAction(null);
+ setDeletionReason('');
}
};
const cancelAdminAction = () => {
setShowAdminModal(false);
setAdminAction(null);
+ setDeletionReason('');
};
const formatDate = (dateString: string) => {
@@ -520,7 +529,26 @@ const ForumPostDetail: React.FC = () => {
{adminAction?.type === 'deletePost' && (
-
Are you sure you want to delete this post? It will be deleted and hidden from regular users but can be restored later.
+ <>
+
Are you sure you want to delete this post? It will be deleted and hidden from regular users but can be restored later.
+
+
+ Reason for deletion *
+
+
+ >
)}
{adminAction?.type === 'restorePost' && (
Are you sure you want to restore this post? It will become visible to all users again.
@@ -532,7 +560,26 @@ const ForumPostDetail: React.FC = () => {
Are you sure you want to reopen this discussion? Users will be able to add comments again.
)}
{adminAction?.type === 'deleteComment' && (
-
Are you sure you want to delete this comment? It will be deleted and hidden from regular users but can be restored later.
+ <>
+
Are you sure you want to delete this comment? It will be deleted and hidden from regular users but can be restored later.
+
+
+ Reason for deletion *
+
+
+ >
)}
{adminAction?.type === 'restoreComment' && (
Are you sure you want to restore this comment? It will become visible to all users again.
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index f5713a7..cb9ff54 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -286,9 +286,9 @@ export const forumAPI = {
deleteComment: (commentId: string) =>
api.delete(`/forum/comments/${commentId}`),
// Admin endpoints
- adminDeletePost: (id: string) => api.delete(`/forum/admin/posts/${id}`),
+ adminDeletePost: (id: string, reason: string) => api.delete(`/forum/admin/posts/${id}`, { data: { reason } }),
adminRestorePost: (id: string) => api.patch(`/forum/admin/posts/${id}/restore`),
- adminDeleteComment: (id: string) => api.delete(`/forum/admin/comments/${id}`),
+ adminDeleteComment: (id: string, reason: string) => api.delete(`/forum/admin/comments/${id}`, { data: { reason } }),
adminRestoreComment: (id: string) => api.patch(`/forum/admin/comments/${id}/restore`),
adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`),
adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`),