deletion reason and email for soft deleted forum posts and comments by admin

This commit is contained in:
jackiettran
2025-11-20 18:08:30 -05:00
parent b2f18d77f6
commit f7767dfd13
10 changed files with 911 additions and 21 deletions

View File

@@ -55,6 +55,10 @@ const ForumComment = sequelize.define('ForumComment', {
deletedAt: { deletedAt: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true
},
deletionReason: {
type: DataTypes.TEXT,
allowNull: true
} }
}); });

View File

@@ -85,6 +85,10 @@ const ForumPost = sequelize.define('ForumPost', {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true
}, },
deletionReason: {
type: DataTypes.TEXT,
allowNull: true
},
closedBy: { closedBy: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true, allowNull: true,

View File

@@ -1250,26 +1250,68 @@ router.get('/tags', async (req, res) => {
// DELETE /api/forum/admin/posts/:id - Admin soft-delete post // DELETE /api/forum/admin/posts/:id - Admin soft-delete post
router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, res) => { router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, res) => {
try { 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) { if (!post) {
return res.status(404).json({ error: 'Post not found' }); 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 // Soft delete the post
await post.update({ await post.update({
isDeleted: true, isDeleted: true,
deletedBy: req.user.id, deletedBy: req.user.id,
deletedAt: new Date() deletedAt: new Date(),
deletionReason: reason.trim()
}); });
const reqLogger = logger.withRequestId(req.id); const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Admin deleted post", { reqLogger.info("Admin deleted post", {
postId: req.params.id, postId: req.params.id,
adminId: req.user.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 }); res.status(200).json({ message: 'Post deleted successfully', post });
} catch (error) { } catch (error) {
const reqLogger = logger.withRequestId(req.id); 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' }); 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 // Restore the post
await post.update({ await post.update({
isDeleted: false, isDeleted: false,
deletedBy: null, deletedBy: null,
deletedAt: null deletedAt: null,
deletionReason: null
}); });
const reqLogger = logger.withRequestId(req.id); 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 // DELETE /api/forum/admin/comments/:id - Admin soft-delete comment
router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req, res) => { router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req, res) => {
try { 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) { if (!comment) {
return res.status(404).json({ error: 'Comment not found' }); 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) // Soft delete the comment (preserve original content for potential restoration)
await comment.update({ await comment.update({
isDeleted: true, isDeleted: true,
deletedBy: req.user.id, deletedBy: req.user.id,
deletedAt: new Date() deletedAt: new Date(),
deletionReason: reason.trim()
}); });
// Decrement comment count if not already deleted // Decrement comment count
if (!comment.isDeleted) { const post = await ForumPost.findByPk(comment.postId, {
const post = await ForumPost.findByPk(comment.postId); attributes: ['id', 'title']
if (post && post.commentCount > 0) { });
await post.decrement('commentCount'); if (post && post.commentCount > 0) {
} await post.decrement('commentCount');
} }
const reqLogger = logger.withRequestId(req.id); const reqLogger = logger.withRequestId(req.id);
@@ -1348,9 +1414,32 @@ router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req
commentId: req.params.id, commentId: req.params.id,
adminId: req.user.id, adminId: req.user.id,
originalAuthorId: comment.authorId, 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 }); res.status(200).json({ message: 'Comment deleted successfully', comment });
} catch (error) { } catch (error) {
const reqLogger = logger.withRequestId(req.id); const reqLogger = logger.withRequestId(req.id);
@@ -1381,7 +1470,8 @@ router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, asy
await comment.update({ await comment.update({
isDeleted: false, isDeleted: false,
deletedBy: null, deletedBy: null,
deletedAt: null deletedAt: null,
deletionReason: null
}); });
// Increment comment count // Increment comment count

View File

@@ -483,7 +483,8 @@ router.patch("/admin/:id/restore", authenticateToken, requireAdmin, async (req,
await item.update({ await item.update({
isDeleted: false, isDeleted: false,
deletedBy: null, deletedBy: null,
deletedAt: null deletedAt: null,
deletionReason: null
}); });
const updatedItem = await Item.findByPk(item.id, { const updatedItem = await Item.findByPk(item.id, {

View File

@@ -65,6 +65,8 @@ class TemplateManager {
"forumThreadActivityToParticipant.html", "forumThreadActivityToParticipant.html",
"forumPostClosed.html", "forumPostClosed.html",
"forumItemRequestNotification.html", "forumItemRequestNotification.html",
"forumPostDeletionToAuthor.html",
"forumCommentDeletionToAuthor.html",
]; ];
for (const templateFile of templateFiles) { for (const templateFile of templateFiles) {

View File

@@ -9,6 +9,7 @@ const TemplateManager = require("../core/TemplateManager");
* - Sending answer accepted notifications * - Sending answer accepted notifications
* - Sending thread activity notifications to participants * - Sending thread activity notifications to participants
* - Sending location-based item request notifications to nearby users * - Sending location-based item request notifications to nearby users
* - Sending post/comment deletion notifications to authors
*/ */
class ForumEmailService { class ForumEmailService {
constructor() { 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 * Send notification to nearby users about an item request
* @param {Object} recipient - Recipient user object * @param {Object} recipient - Recipient user object

View File

@@ -0,0 +1,314 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Your Forum Comment Has Been Removed</title>
<style>
/* Reset styles */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header - Warning red gradient */
.header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #f8d7da;
font-size: 16px;
margin-top: 8px;
font-weight: 600;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 28px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 22px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Warning box */
.warning-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.warning-box p {
margin: 0 0 10px 0;
color: #856404;
}
.warning-box p:last-child {
margin-bottom: 0;
}
/* Alert box */
.alert-box {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.alert-box p {
margin: 0 0 10px 0;
color: #721c24;
}
.alert-box p:last-child {
margin-bottom: 0;
}
/* Post highlight */
.post-highlight {
background-color: #f8f9fa;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
}
.post-highlight .post-title {
font-size: 18px;
font-weight: 600;
color: #495057;
margin-bottom: 5px;
}
.post-highlight .post-link {
font-size: 14px;
color: #667eea;
text-decoration: none;
}
.post-highlight .post-link:hover {
text-decoration: underline;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #667eea;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0 0 10px 0;
color: #004085;
}
.info-box p:last-child {
margin-bottom: 0;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header, .content, .footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 24px;
}
.content h2 {
font-size: 20px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.post-highlight .post-title {
font-size: 16px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">⚠️ Important: Comment Removal Notice</div>
</div>
<div class="content">
<p>Hi {{commentAuthorName}},</p>
<h1>Your Comment Has Been Removed</h1>
<p>We're writing to inform you that your comment has been removed from a forum discussion by {{adminName}}.</p>
<div class="post-highlight">
<p style="margin: 0 0 10px 0; color: #6c757d; font-size: 14px;">Comment on:</p>
<div class="post-title">{{postTitle}}</div>
<a href="{{postUrl}}" class="post-link">View Discussion →</a>
</div>
<div class="alert-box">
<p><strong>Reason for Removal:</strong></p>
<p>{{deletionReason}}</p>
</div>
<div class="info-box">
<p><strong>What this means:</strong></p>
<ul style="margin: 10px 0; padding-left: 20px; color: #004085;">
<li>Your comment is no longer visible to other community members</li>
<li>The comment content has been preserved in case of appeal</li>
<li>The discussion thread remains active for other participants</li>
<li>You can still participate in other forum discussions</li>
</ul>
</div>
<h2>Need Help or Have Questions?</h2>
<p>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:</p>
<p style="text-align: center;">
<a href="mailto:{{supportEmail}}" class="button">Contact Support</a>
</p>
<div class="warning-box">
<p><strong>Review Our Community Guidelines:</strong></p>
<p>To ensure a positive experience for all members, please review our community guidelines. We appreciate respectful, constructive contributions that help build a supportive community.</p>
</div>
<p>Thank you for your understanding, and we look forward to your continued participation in our community.</p>
<p><strong>Best regards,</strong><br>
The RentAll Team</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>Building a community of sharing and trust</p>
<p>This email was sent because your comment was removed by our moderation team.</p>
<p>If you have questions, please contact <a href="mailto:{{supportEmail}}">{{supportEmail}}</a></p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,305 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Your Forum Post Has Been Removed</title>
<style>
/* Reset styles */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header - Warning red gradient */
.header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #f8d7da;
font-size: 16px;
margin-top: 8px;
font-weight: 600;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 28px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 22px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Warning box */
.warning-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.warning-box p {
margin: 0 0 10px 0;
color: #856404;
}
.warning-box p:last-child {
margin-bottom: 0;
}
/* Alert box */
.alert-box {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.alert-box p {
margin: 0 0 10px 0;
color: #721c24;
}
.alert-box p:last-child {
margin-bottom: 0;
}
/* Post highlight */
.post-highlight {
background-color: #f8f9fa;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.post-highlight .post-title {
font-size: 20px;
font-weight: 600;
color: #dc3545;
margin-bottom: 10px;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #667eea;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0 0 10px 0;
color: #004085;
}
.info-box p:last-child {
margin-bottom: 0;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header, .content, .footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 24px;
}
.content h2 {
font-size: 20px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.post-highlight .post-title {
font-size: 18px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">⚠️ Important: Forum Post Removal Notice</div>
</div>
<div class="content">
<p>Hi {{postAuthorName}},</p>
<h1>Your Forum Post Has Been Removed</h1>
<p>We're writing to inform you that your forum post has been removed from RentAll by {{adminName}}.</p>
<div class="post-highlight">
<div class="post-title">{{postTitle}}</div>
</div>
<div class="alert-box">
<p><strong>Reason for Removal:</strong></p>
<p>{{deletionReason}}</p>
</div>
<div class="info-box">
<p><strong>What this means:</strong></p>
<ul style="margin: 10px 0; padding-left: 20px; color: #004085;">
<li>Your post is no longer visible to other community members</li>
<li>All comments on this post are also hidden</li>
<li>The post cannot receive new comments or activity</li>
<li>You may still see it in your dashboard if viewing as an admin</li>
</ul>
</div>
<h2>Need Help or Have Questions?</h2>
<p>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:</p>
<p style="text-align: center;">
<a href="mailto:{{supportEmail}}" class="button">Contact Support</a>
</p>
<div class="warning-box">
<p><strong>Review Our Community Guidelines:</strong></p>
<p>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.</p>
</div>
<p>You can continue participating in the forum by visiting our <a href="{{forumUrl}}" style="color: #667eea;">community forum</a>.</p>
<p>Thank you for your understanding.</p>
<p><strong>Best regards,</strong><br>
The RentAll Team</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>Building a community of sharing and trust</p>
<p>This email was sent because your forum post was removed by our moderation team.</p>
<p>If you have questions, please contact <a href="mailto:{{supportEmail}}">{{supportEmail}}</a></p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -23,6 +23,7 @@ const ForumPostDetail: React.FC = () => {
type: 'deletePost' | 'deleteComment' | 'restorePost' | 'restoreComment' | 'closePost' | 'reopenPost'; type: 'deletePost' | 'deleteComment' | 'restorePost' | 'restoreComment' | 'closePost' | 'reopenPost';
id?: string; id?: string;
} | null>(null); } | null>(null);
const [deletionReason, setDeletionReason] = useState('');
// Read filter from URL query param // Read filter from URL query param
const filter = searchParams.get('filter') || 'active'; const filter = searchParams.get('filter') || 'active';
@@ -174,13 +175,19 @@ const ForumPostDetail: React.FC = () => {
const confirmAdminAction = async () => { const confirmAdminAction = async () => {
if (!adminAction) return; 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 { try {
setActionLoading(true); setActionLoading(true);
setShowAdminModal(false); setShowAdminModal(false);
switch (adminAction.type) { switch (adminAction.type) {
case 'deletePost': case 'deletePost':
await forumAPI.adminDeletePost(id!); await forumAPI.adminDeletePost(id!, deletionReason.trim());
break; break;
case 'restorePost': case 'restorePost':
await forumAPI.adminRestorePost(id!); await forumAPI.adminRestorePost(id!);
@@ -192,7 +199,7 @@ const ForumPostDetail: React.FC = () => {
await forumAPI.adminReopenPost(id!); await forumAPI.adminReopenPost(id!);
break; break;
case 'deleteComment': case 'deleteComment':
await forumAPI.adminDeleteComment(adminAction.id!); await forumAPI.adminDeleteComment(adminAction.id!, deletionReason.trim());
break; break;
case 'restoreComment': case 'restoreComment':
await forumAPI.adminRestoreComment(adminAction.id!); await forumAPI.adminRestoreComment(adminAction.id!);
@@ -205,12 +212,14 @@ const ForumPostDetail: React.FC = () => {
} finally { } finally {
setActionLoading(false); setActionLoading(false);
setAdminAction(null); setAdminAction(null);
setDeletionReason('');
} }
}; };
const cancelAdminAction = () => { const cancelAdminAction = () => {
setShowAdminModal(false); setShowAdminModal(false);
setAdminAction(null); setAdminAction(null);
setDeletionReason('');
}; };
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
@@ -520,7 +529,26 @@ const ForumPostDetail: React.FC = () => {
</div> </div>
<div className="modal-body"> <div className="modal-body">
{adminAction?.type === 'deletePost' && ( {adminAction?.type === 'deletePost' && (
<p>Are you sure you want to delete this post? It will be deleted and hidden from regular users but can be restored later.</p> <>
<p>Are you sure you want to delete this post? It will be deleted and hidden from regular users but can be restored later.</p>
<div className="mb-3">
<label htmlFor="deletionReason" className="form-label">
<strong>Reason for deletion <span className="text-danger">*</span></strong>
</label>
<textarea
id="deletionReason"
className="form-control"
rows={4}
placeholder="Please explain why this post is being deleted. The author will receive this reason via email."
value={deletionReason}
onChange={(e) => setDeletionReason(e.target.value)}
required
/>
<small className="text-muted">
This reason will be sent to the post author via email.
</small>
</div>
</>
)} )}
{adminAction?.type === 'restorePost' && ( {adminAction?.type === 'restorePost' && (
<p>Are you sure you want to restore this post? It will become visible to all users again.</p> <p>Are you sure you want to restore this post? It will become visible to all users again.</p>
@@ -532,7 +560,26 @@ const ForumPostDetail: React.FC = () => {
<p>Are you sure you want to reopen this discussion? Users will be able to add comments again.</p> <p>Are you sure you want to reopen this discussion? Users will be able to add comments again.</p>
)} )}
{adminAction?.type === 'deleteComment' && ( {adminAction?.type === 'deleteComment' && (
<p>Are you sure you want to delete this comment? It will be deleted and hidden from regular users but can be restored later.</p> <>
<p>Are you sure you want to delete this comment? It will be deleted and hidden from regular users but can be restored later.</p>
<div className="mb-3">
<label htmlFor="deletionReason" className="form-label">
<strong>Reason for deletion <span className="text-danger">*</span></strong>
</label>
<textarea
id="deletionReason"
className="form-control"
rows={4}
placeholder="Please explain why this comment is being deleted. The author will receive this reason via email."
value={deletionReason}
onChange={(e) => setDeletionReason(e.target.value)}
required
/>
<small className="text-muted">
This reason will be sent to the comment author via email.
</small>
</div>
</>
)} )}
{adminAction?.type === 'restoreComment' && ( {adminAction?.type === 'restoreComment' && (
<p>Are you sure you want to restore this comment? It will become visible to all users again.</p> <p>Are you sure you want to restore this comment? It will become visible to all users again.</p>

View File

@@ -286,9 +286,9 @@ export const forumAPI = {
deleteComment: (commentId: string) => deleteComment: (commentId: string) =>
api.delete(`/forum/comments/${commentId}`), api.delete(`/forum/comments/${commentId}`),
// Admin endpoints // 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`), 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`), adminRestoreComment: (id: string) => api.patch(`/forum/admin/comments/${id}/restore`),
adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`), adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`),
adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`), adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`),