feat(backend): implement comments, image uploads, and data consistency
Implement additional backend features and improve data models: Comments System: - Create Comment model with user and post relationships - Add comments routes: GET /api/posts/:postId/comments (paginated), POST (create), DELETE (own comments) - Update Post model with commentsCount field - Emit Socket.IO events for newComment and commentDeleted - Pagination support for comment lists - Authorization checks (users can only delete own comments) - 500 character limit on comments Image Upload System: - Implement Cloudinary configuration (config/cloudinary.js) - Add uploadImage() and deleteImage() helper functions - Image optimization: max 1000x1000, auto quality, auto format (WebP) - Integrate image upload in users routes (profile pictures) - Integrate image upload in posts routes (post images with add/update endpoints) - File validation: 5MB limit, JPG/PNG/GIF/WebP only - Automatic image deletion when removing posts/reports Data Consistency Improvements: - Add cascade deletes in Street model (remove from user, delete associated tasks) - Add cascade deletes in Task model (remove from user completedTasks) - Add cascade deletes in Post model (remove from user posts) - Update user relationships on save (adoptedStreets, completedTasks, posts, events) - Add proper indexes for performance (2dsphere for location, compound indexes) - Add virtual relationships and toJSON configurations Model Updates: - Street: Add cascade hooks, location 2dsphere index - Task: Add cascade hooks, compound indexes for queries - Post: Add imageUrl, cloudinaryPublicId, commentsCount fields - Event: Add participants tracking - Report: Add image upload support - User: Add earnedBadges virtual, profilePicture, cloudinaryPublicId 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
146
backend/routes/comments.js
Normal file
146
backend/routes/comments.js
Normal file
@@ -0,0 +1,146 @@
|
||||
const express = require("express");
|
||||
const mongoose = require("mongoose");
|
||||
const Comment = require("../models/Comment");
|
||||
const Post = require("../models/Post");
|
||||
const auth = require("../middleware/auth");
|
||||
const { paginate, buildPaginatedResponse } = require("../middleware/pagination");
|
||||
const { asyncHandler } = require("../middleware/errorHandler");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/posts/:postId/comments
|
||||
* Get all comments for a post (paginated)
|
||||
*/
|
||||
router.get(
|
||||
"/:postId/comments",
|
||||
paginate,
|
||||
asyncHandler(async (req, res) => {
|
||||
const { postId } = req.params;
|
||||
const { skip, limit, page } = req.pagination;
|
||||
|
||||
// Verify post exists
|
||||
const post = await Post.findById(postId);
|
||||
if (!post) {
|
||||
return res.status(404).json({ msg: "Post not found" });
|
||||
}
|
||||
|
||||
// Get comments with pagination
|
||||
const comments = await Comment.find({ post: postId })
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.populate("user", ["name", "profilePicture"]);
|
||||
|
||||
const totalCount = await Comment.countDocuments({ post: postId });
|
||||
|
||||
res.json(buildPaginatedResponse(comments, totalCount, page, limit));
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/posts/:postId/comments
|
||||
* Create a new comment on a post
|
||||
*/
|
||||
router.post(
|
||||
"/:postId/comments",
|
||||
auth,
|
||||
asyncHandler(async (req, res) => {
|
||||
const { postId } = req.params;
|
||||
const { content } = req.body;
|
||||
|
||||
// Validate content
|
||||
if (!content || content.trim().length === 0) {
|
||||
return res.status(400).json({ msg: "Comment content is required" });
|
||||
}
|
||||
|
||||
if (content.length > 500) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ msg: "Comment content must be 500 characters or less" });
|
||||
}
|
||||
|
||||
// Verify post exists
|
||||
const post = await Post.findById(postId);
|
||||
if (!post) {
|
||||
return res.status(404).json({ msg: "Post not found" });
|
||||
}
|
||||
|
||||
// Create comment
|
||||
const newComment = new Comment({
|
||||
user: req.user.id,
|
||||
post: postId,
|
||||
content: content.trim(),
|
||||
});
|
||||
|
||||
const comment = await newComment.save();
|
||||
|
||||
// Update post's comment count
|
||||
await Post.findByIdAndUpdate(postId, {
|
||||
$inc: { commentsCount: 1 },
|
||||
});
|
||||
|
||||
// Populate user data before sending response
|
||||
await comment.populate("user", ["name", "profilePicture"]);
|
||||
|
||||
// Emit Socket.IO event for new comment
|
||||
const io = req.app.get("io");
|
||||
if (io) {
|
||||
io.to(`post_${postId}`).emit("newComment", {
|
||||
postId,
|
||||
comment,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json(comment);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/posts/:postId/comments/:commentId
|
||||
* Delete own comment
|
||||
*/
|
||||
router.delete(
|
||||
"/:postId/comments/:commentId",
|
||||
auth,
|
||||
asyncHandler(async (req, res) => {
|
||||
const { postId, commentId } = req.params;
|
||||
|
||||
// Find comment
|
||||
const comment = await Comment.findById(commentId);
|
||||
if (!comment) {
|
||||
return res.status(404).json({ msg: "Comment not found" });
|
||||
}
|
||||
|
||||
// Verify comment belongs to the post
|
||||
if (comment.post.toString() !== postId) {
|
||||
return res.status(400).json({ msg: "Comment does not belong to this post" });
|
||||
}
|
||||
|
||||
// Verify user owns the comment
|
||||
if (comment.user.toString() !== req.user.id) {
|
||||
return res.status(403).json({ msg: "Not authorized to delete this comment" });
|
||||
}
|
||||
|
||||
// Delete comment
|
||||
await Comment.findByIdAndDelete(commentId);
|
||||
|
||||
// Update post's comment count
|
||||
await Post.findByIdAndUpdate(postId, {
|
||||
$inc: { commentsCount: -1 },
|
||||
});
|
||||
|
||||
// Emit Socket.IO event for deleted comment
|
||||
const io = req.app.get("io");
|
||||
if (io) {
|
||||
io.to(`post_${postId}`).emit("commentDeleted", {
|
||||
postId,
|
||||
commentId,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ msg: "Comment deleted successfully" });
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user