express-autotemplates
Version:
CLI tool to generate Express backend projects with various templates
1,073 lines (902 loc) • 31.7 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const packageJson = (projectName) => ({
name: projectName,
version: "1.0.0",
description: "Blog backend with posts and comments",
main: "server.js",
scripts: {
start: "node server.js",
dev: "nodemon server.js",
test: "echo \"Error: no test specified\" && exit 1"
},
keywords: ["express", "blog", "mongodb", "jwt", "api"],
author: "",
license: "MIT",
engines: {
node: ">=16.0.0"
},
dependencies: {
express: "^4.21.2",
mongoose: "^8.16.5",
bcryptjs: "^3.0.2",
jsonwebtoken: "^9.0.2",
cors: "^2.8.5",
dotenv: "^17.2.1",
slugify: "^1.6.6",
helmet: "^8.1.0",
morgan: "^1.10.0",
compression: "^1.7.4",
validator: "^13.15.15",
multer: "^2.0.2",
"express-rate-limit": "^8.0.1",
"express-validator": "^7.2.1",
nodemailer: "^7.0.5"
},
devDependencies: {
nodemon: "^3.1.10"
}
});
const serverJs = `const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
// Security middleware
app.use(helmet());
app.use(morgan('combined'));
app.use(compression());
app.use(limiter);
// CORS middleware
app.use(cors({
origin: process.env.CLIENT_URL || '*',
credentials: true
}));
// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Static files for uploads
app.use('/uploads', express.static('uploads'));
// MongoDB connection
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/blog')
.then(() => console.log('✅ Connected to MongoDB'))
.catch(err => console.error('❌ MongoDB connection error:', err));
// Import routes
const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/posts');
const commentRoutes = require('./routes/comments');
// Routes
app.get('/', (req, res) => {
res.json({
message: 'Blog Backend API',
version: '1.0.0',
endpoints: {
auth: '/api/auth',
posts: '/api/posts',
comments: '/api/comments'
}
});
});
app.use('/api/auth', authRoutes);
app.use('/api/posts', postRoutes);
app.use('/api/comments', commentRoutes);
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production' ? 'Something went wrong!' : err.message
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({ error: 'Route not found' });
});
app.listen(PORT, () => {
console.log(\`🚀 Blog server running on port \${PORT}\`);
console.log(\`📍 Environment: \${process.env.NODE_ENV || 'development'}\`);
console.log(\`🌐 URL: http://localhost:\${PORT}\`);
});`;
const userModel = `const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
firstName: String,
lastName: String,
bio: String,
avatar: String,
role: { type: String, enum: ['user', 'admin'], default: 'user' }
}, { timestamps: true });
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
userSchema.methods.comparePassword = async function(password) {
return bcrypt.compare(password, this.password);
};
module.exports = mongoose.model('User', userSchema);`;
const postModel = `const mongoose = require('mongoose');
const slugify = require('slugify');
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
slug: { type: String, unique: true },
content: { type: String, required: true },
excerpt: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
tags: [String],
category: String,
status: { type: String, enum: ['draft', 'published'], default: 'draft' },
featuredImage: String,
views: { type: Number, default: 0 },
likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
publishedAt: Date
}, { timestamps: true });
postSchema.pre('save', function(next) {
if (this.isModified('title')) {
this.slug = slugify(this.title, { lower: true, strict: true });
}
if (this.status === 'published' && !this.publishedAt) {
this.publishedAt = new Date();
}
next();
});
module.exports = mongoose.model('Post', postSchema);`;
const commentModel = `const mongoose = require('mongoose');
const commentSchema = new mongoose.Schema({
content: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
parentComment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' },
likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
status: { type: String, enum: ['approved', 'pending', 'spam'], default: 'pending' }
}, { timestamps: true });
module.exports = mongoose.model('Comment', commentSchema);`;
const envFile = `PORT=3000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/blog
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRE=7d
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your-email@gmail.com
EMAIL_PASS=your-app-password
CLIENT_URL=http://localhost:3000`;
const readmeFile = (projectName) => `# ${projectName}
Professional blog backend with complete post management, nested comments, user authentication, and admin features.
## Features
- 🔐 User authentication with JWT (user/admin roles)
- 📝 Complete blog post CRUD with draft/published status
- 💬 Nested comment system with moderation
- 🖼️ Featured image uploads for posts
- ⭐ Like system for posts and comments
- 🔍 Advanced search and filtering
- 📊 Pagination for all listings
- 🏷️ Post categories and tags system
- 👀 Post view tracking
- 🛡️ Security middleware (Helmet, Rate limiting)
- 📧 Email notifications ready (Nodemailer)
- 📈 Request logging with Morgan
- ✅ Input validation with express-validator
- 🌐 SEO-friendly URL slugs
## Prerequisites
- MongoDB installed and running
- Node.js (v14 or higher)
## Getting Started
1. Install dependencies:
\`\`\`bash
npm install
\`\`\`
2. Update the \`.env\` file with your configuration:
\`\`\`env
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
MONGODB_URI=mongodb://localhost:27017/blog
EMAIL_USER=your-email@gmail.com
EMAIL_PASS=your-app-password
\`\`\`
3. Start the server:
\`\`\`bash
npm start
\`\`\`
For development with auto-reload:
\`\`\`bash
npm run dev
\`\`\`
## API Endpoints
### Authentication
- \`POST /api/auth/register\` - Register new user
\`\`\`json
{
"username": "johndoe",
"email": "john@example.com",
"password": "password123",
"firstName": "John",
"lastName": "Doe"
}
\`\`\`
- \`POST /api/auth/login\` - Login user
\`\`\`json
{
"email": "john@example.com",
"password": "password123"
}
\`\`\`
- \`GET /api/auth/profile\` - Get user profile (authenticated)
- \`PUT /api/auth/profile\` - Update user profile (authenticated)
### Posts
- \`GET /api/posts?page=1&limit=10&category=tech&search=javascript\` - Get published posts with filtering
- \`GET /api/posts/:slug\` - Get single post by slug (increments view count)
- \`GET /api/posts/user/my-posts?status=draft\` - Get user's own posts (authenticated)
- \`POST /api/posts\` - Create new post with image upload (authenticated)
\`\`\`javascript
// Form data with file upload
const formData = new FormData();
formData.append('title', 'My Blog Post');
formData.append('content', 'Post content here...');
formData.append('excerpt', 'Short description');
formData.append('category', 'Technology');
formData.append('tags', 'javascript,nodejs,express');
formData.append('status', 'published');
formData.append('featuredImage', imageFile);
\`\`\`
- \`PUT /api/posts/:id\` - Update post (author/admin)
- \`DELETE /api/posts/:id\` - Delete post (author/admin)
- \`POST /api/posts/:id/like\` - Like/unlike post (authenticated)
### Comments
- \`GET /api/comments/post/:postId?page=1&limit=20\` - Get comments for post with replies
- \`POST /api/comments\` - Create comment or reply (authenticated)
\`\`\`json
{
"content": "Great post!",
"post": "post-id-here",
"parentComment": "parent-comment-id-for-replies"
}
\`\`\`
- \`PUT /api/comments/:id\` - Update comment (author/admin)
- \`DELETE /api/comments/:id\` - Delete comment and replies (author/admin)
- \`POST /api/comments/:id/like\` - Like/unlike comment (authenticated)
### Admin Comment Management
- \`GET /api/comments/admin/pending\` - Get pending comments (admin)
- \`PUT /api/comments/:id/approve\` - Approve comment (admin)
## Database Schema
### User
- username (unique), email (unique), password (hashed)
- firstName, lastName, bio, avatar
- role (user/admin)
### Post
- title, slug (auto-generated), content, excerpt
- author (User reference), tags[], category
- status (draft/published), featuredImage
- views, likes[] (User references)
- publishedAt (auto-set when published)
### Comment
- content, author (User reference)
- post (Post reference), parentComment (Comment reference)
- likes[] (User references)
- status (approved/pending/spam)
## Usage Examples
### Create Blog Post:
\`\`\`javascript
const formData = new FormData();
formData.append('title', 'Getting Started with Node.js');
formData.append('content', 'Node.js is a powerful runtime...');
formData.append('excerpt', 'Learn the basics of Node.js');
formData.append('category', 'Programming');
formData.append('tags', 'nodejs,javascript,backend');
formData.append('status', 'published');
formData.append('featuredImage', imageFile);
fetch('/api/posts', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token
},
body: formData
});
\`\`\`
### Add Nested Comment:
\`\`\`javascript
// Reply to a comment
fetch('/api/comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
content: 'Thanks for the explanation!',
post: 'post-id-here',
parentComment: 'parent-comment-id'
})
});
\`\`\`
### Search Posts:
\`\`\`javascript
// Search posts by title/content
fetch('/api/posts?search=javascript&category=programming&page=1&limit=5')
.then(res => res.json())
.then(data => {
console.log('Posts:', data.posts);
console.log('Pagination:', data.pagination);
});
\`\`\`
## Security Features
- Password hashing with bcrypt
- JWT token authentication
- Role-based authorization (user/admin)
- Input validation and sanitization
- Rate limiting (100 requests per 15 minutes)
- CORS protection
- Helmet security headers
- File upload restrictions (images only, 5MB limit)
## File Upload
- Featured images stored in \`/uploads\` directory
- 5MB file size limit
- Only image files allowed
- Automatic filename generation to prevent conflicts
## Comment Moderation
- Comments require approval by default (except admin)
- Nested reply system
- Admin can approve/reject comments
- Spam detection ready
## SEO Features
- Auto-generated slugs from post titles
- View count tracking
- Meta descriptions and titles ready
- Clean URLs for posts
Server runs on http://localhost:3000
`;
async function generate(projectPath, projectName) {
// Create main files
await fs.writeJson(path.join(projectPath, 'package.json'), packageJson(projectName), { spaces: 2 });
await fs.writeFile(path.join(projectPath, 'server.js'), serverJs);
await fs.writeFile(path.join(projectPath, '.env'), envFile);
await fs.writeFile(path.join(projectPath, 'README.md'), readmeFile(projectName));
await fs.writeFile(path.join(projectPath, '.gitignore'), 'node_modules/\n.env\n*.log\n');
// Create directories
await fs.ensureDir(path.join(projectPath, 'models'));
await fs.ensureDir(path.join(projectPath, 'routes'));
await fs.ensureDir(path.join(projectPath, 'middleware'));
// Create models
await fs.writeFile(path.join(projectPath, 'models/User.js'), userModel);
await fs.writeFile(path.join(projectPath, 'models/Post.js'), postModel);
await fs.writeFile(path.join(projectPath, 'models/Comment.js'), commentModel);
// Create basic auth middleware
const authMiddleware = `const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'Invalid token.' });
}
req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token.' });
}
};
module.exports = auth;`;
await fs.writeFile(path.join(projectPath, 'middleware/auth.js'), authMiddleware);
// Complete auth routes
const authRoutes = `const express = require('express');
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
const auth = require('../middleware/auth');
const router = express.Router();
// Register user
router.post('/register', [
body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
body('email').isEmail().withMessage('Please provide a valid email'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, email, password, firstName, lastName } = req.body;
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
if (existingUser) {
return res.status(400).json({ error: 'User with this email or username already exists' });
}
const user = new User({ username, email, password, firstName, lastName });
await user.save();
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRE });
res.status(201).json({
token,
user: {
id: user._id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role
}
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Login user
router.post('/login', [
body('email').isEmail().withMessage('Please provide a valid email'),
body('password').notEmpty().withMessage('Password is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRE });
res.json({
token,
user: {
id: user._id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role
}
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Get user profile
router.get('/profile', auth, async (req, res) => {
try {
const user = await User.findById(req.user._id).select('-password');
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update user profile
router.put('/profile', auth, [
body('email').optional().isEmail().withMessage('Please provide a valid email'),
body('username').optional().isLength({ min: 3 }).withMessage('Username must be at least 3 characters')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const updates = req.body;
delete updates.password; // Don't allow password updates through this route
const user = await User.findByIdAndUpdate(req.user._id, updates, { new: true }).select('-password');
res.json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
module.exports = router;`;
// Complete post routes
const postRoutes = `const express = require('express');
const multer = require('multer');
const path = require('path');
const { body, validationResult } = require('express-validator');
const Post = require('../models/Post');
const auth = require('../middleware/auth');
const router = express.Router();
// Multer configuration for featured images
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed!'), false);
}
}
});
// Get all published posts with pagination and filtering
router.get('/', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const filter = { status: 'published' };
if (req.query.category) filter.category = req.query.category;
if (req.query.tag) filter.tags = { $in: [req.query.tag] };
if (req.query.author) filter.author = req.query.author;
if (req.query.search) {
filter.$or = [
{ title: { $regex: req.query.search, $options: 'i' } },
{ content: { $regex: req.query.search, $options: 'i' } },
{ excerpt: { $regex: req.query.search, $options: 'i' } }
];
}
const posts = await Post.find(filter)
.populate('author', 'username firstName lastName avatar')
.sort({ publishedAt: -1 })
.skip(skip)
.limit(limit)
.select('-content'); // Don't send full content in list view
const total = await Post.countDocuments(filter);
res.json({
posts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get single post by slug
router.get('/:slug', async (req, res) => {
try {
const post = await Post.findOne({ slug: req.params.slug, status: 'published' })
.populate('author', 'username firstName lastName avatar bio');
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// Increment view count
post.views += 1;
await post.save();
res.json(post);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get user's own posts (including drafts)
router.get('/user/my-posts', auth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const filter = { author: req.user._id };
if (req.query.status) filter.status = req.query.status;
const posts = await Post.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await Post.countDocuments(filter);
res.json({
posts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create new post
router.post('/', auth, upload.single('featuredImage'), [
body('title').notEmpty().withMessage('Title is required'),
body('content').notEmpty().withMessage('Content is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const postData = {
...req.body,
author: req.user._id
};
if (req.file) {
postData.featuredImage = req.file.filename;
}
if (postData.tags && typeof postData.tags === 'string') {
postData.tags = postData.tags.split(',').map(tag => tag.trim());
}
const post = new Post(postData);
await post.save();
await post.populate('author', 'username firstName lastName');
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Update post
router.put('/:id', auth, upload.single('featuredImage'), async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// Check if user is author or admin
if (post.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const updateData = req.body;
if (req.file) {
updateData.featuredImage = req.file.filename;
}
if (updateData.tags && typeof updateData.tags === 'string') {
updateData.tags = updateData.tags.split(',').map(tag => tag.trim());
}
const updatedPost = await Post.findByIdAndUpdate(req.params.id, updateData, { new: true })
.populate('author', 'username firstName lastName');
res.json(updatedPost);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Delete post
router.delete('/:id', auth, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// Check if user is author or admin
if (post.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
await Post.findByIdAndDelete(req.params.id);
res.json({ message: 'Post deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Like/unlike post
router.post('/:id/like', auth, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
const userLikedIndex = post.likes.indexOf(req.user._id);
if (userLikedIndex > -1) {
// Unlike
post.likes.splice(userLikedIndex, 1);
} else {
// Like
post.likes.push(req.user._id);
}
await post.save();
res.json({
liked: userLikedIndex === -1,
likesCount: post.likes.length
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;`;
// Complete comment routes
const commentRoutes = `const express = require('express');
const { body, validationResult } = require('express-validator');
const Comment = require('../models/Comment');
const Post = require('../models/Post');
const auth = require('../middleware/auth');
const router = express.Router();
// Get comments for a post
router.get('/post/:postId', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
const comments = await Comment.find({
post: req.params.postId,
status: 'approved',
parentComment: null // Only top-level comments
})
.populate('author', 'username firstName lastName avatar')
.populate({
path: 'post',
select: 'title slug'
})
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
// Get replies for each comment
for (let comment of comments) {
const replies = await Comment.find({
parentComment: comment._id,
status: 'approved'
})
.populate('author', 'username firstName lastName avatar')
.sort({ createdAt: 1 });
comment._doc.replies = replies;
}
const total = await Comment.countDocuments({
post: req.params.postId,
status: 'approved',
parentComment: null
});
res.json({
comments,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create comment
router.post('/', auth, [
body('content').notEmpty().withMessage('Comment content is required'),
body('post').notEmpty().withMessage('Post ID is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { content, post, parentComment } = req.body;
// Verify post exists
const postExists = await Post.findById(post);
if (!postExists) {
return res.status(404).json({ error: 'Post not found' });
}
// If replying to a comment, verify parent comment exists
if (parentComment) {
const parentExists = await Comment.findById(parentComment);
if (!parentExists) {
return res.status(404).json({ error: 'Parent comment not found' });
}
}
const comment = new Comment({
content,
author: req.user._id,
post,
parentComment: parentComment || null,
status: req.user.role === 'admin' ? 'approved' : 'pending'
});
await comment.save();
await comment.populate('author', 'username firstName lastName avatar');
res.status(201).json(comment);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Update comment
router.put('/:id', auth, [
body('content').notEmpty().withMessage('Comment content is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({ error: 'Comment not found' });
}
// Check if user is author or admin
if (comment.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
comment.content = req.body.content;
await comment.save();
await comment.populate('author', 'username firstName lastName avatar');
res.json(comment);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Delete comment
router.delete('/:id', auth, async (req, res) => {
try {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({ error: 'Comment not found' });
}
// Check if user is author or admin
if (comment.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
// Delete comment and its replies
await Comment.deleteMany({
$or: [
{ _id: req.params.id },
{ parentComment: req.params.id }
]
});
res.json({ message: 'Comment deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Like/unlike comment
router.post('/:id/like', auth, async (req, res) => {
try {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({ error: 'Comment not found' });
}
const userLikedIndex = comment.likes.indexOf(req.user._id);
if (userLikedIndex > -1) {
// Unlike
comment.likes.splice(userLikedIndex, 1);
} else {
// Like
comment.likes.push(req.user._id);
}
await comment.save();
res.json({
liked: userLikedIndex === -1,
likesCount: comment.likes.length
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Admin: Approve comment
router.put('/:id/approve', auth, async (req, res) => {
try {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
const comment = await Comment.findByIdAndUpdate(
req.params.id,
{ status: 'approved' },
{ new: true }
).populate('author', 'username firstName lastName avatar');
if (!comment) {
return res.status(404).json({ error: 'Comment not found' });
}
res.json(comment);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Admin: Get pending comments
router.get('/admin/pending', auth, async (req, res) => {
try {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
const comments = await Comment.find({ status: 'pending' })
.populate('author', 'username firstName lastName avatar')
.populate('post', 'title slug')
.sort({ createdAt: -1 });
res.json(comments);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;`;
await fs.writeFile(path.join(projectPath, 'routes/auth.js'), authRoutes);
await fs.writeFile(path.join(projectPath, 'routes/posts.js'), postRoutes);
await fs.writeFile(path.join(projectPath, 'routes/comments.js'), commentRoutes);
// Create uploads directory
await fs.ensureDir(path.join(projectPath, 'uploads'));
}
module.exports = { generate };