E-Commerce-Module/backend/src/routes/blogAdmin.js

494 lines
No EOL
14 KiB
JavaScript

const express = require('express');
const { v4: uuidv4 } = require('uuid');
const router = express.Router();
const slugify = require('slugify');
module.exports = (pool, query, authMiddleware) => {
// Apply authentication middleware to all routes
router.use(authMiddleware);
// Get all blog posts (admin)
router.get('/', async (req, res, next) => {
try {
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Get all posts with basic info
const result = await query(`
SELECT
b.id, b.title, b.slug, b.excerpt, b.status,
b.featured_image_path, b.published_at, b.created_at,
u.first_name as author_first_name, u.last_name as author_last_name,
bc.name as category_name
FROM blog_posts b
LEFT JOIN users u ON b.author_id = u.id
LEFT JOIN blog_categories bc ON b.category_id = bc.id
ORDER BY
CASE WHEN b.status = 'draft' THEN 1
WHEN b.status = 'published' THEN 2
ELSE 3
END,
b.created_at DESC
`);
res.json(result.rows);
} catch (error) {
next(error);
}
});
// Get single blog post for editing (admin)
router.get('/:id', async (req, res, next) => {
try {
const { id } = req.params;
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Get post with all details
const postQuery = `
SELECT
b.id, b.title, b.slug, b.content, b.excerpt,
b.featured_image_path, b.status, b.published_at,
b.created_at, b.updated_at, b.author_id, b.category_id,
u.first_name as author_first_name, u.last_name as author_last_name,
bc.name as category_name,
ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags
FROM blog_posts b
LEFT JOIN users u ON b.author_id = u.id
LEFT JOIN blog_categories bc ON b.category_id = bc.id
LEFT JOIN blog_post_tags bpt ON b.id = bpt.post_id
LEFT JOIN tags t ON bpt.tag_id = t.id
WHERE b.id = $1
GROUP BY b.id, u.id, bc.id
`;
const postResult = await query(postQuery, [id]);
if (postResult.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Blog post not found'
});
}
const post = postResult.rows[0];
// Get post images
const imagesQuery = `
SELECT id, image_path, caption, display_order
FROM blog_post_images
WHERE post_id = $1
ORDER BY display_order
`;
const imagesResult = await query(imagesQuery, [id]);
// Return the post with images
res.json({
...post,
images: imagesResult.rows
});
} catch (error) {
next(error);
}
});
// Create a new blog post
router.post('/', async (req, res, next) => {
try {
const {
title, content, excerpt, categoryId,
tags, featuredImagePath, status, publishNow
} = req.body;
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Validate required fields
if (!title || !content) {
return res.status(400).json({
error: true,
message: 'Title and content are required'
});
}
// Generate slug from title
let slug = slugify(title, {
lower: true, // convert to lower case
strict: true, // strip special characters
remove: /[*+~.()'"!:@]/g // regex to remove characters
});
const slugCheck = await query(
'SELECT id FROM blog_posts WHERE slug = $1',
[slug]
);
// If slug exists, append a random string
if (slugCheck.rows.length > 0) {
const randomString = Math.random().toString(36).substring(2, 8);
slug = `${slug}-${randomString}`;
}
// Begin transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
// Set published_at date if publishing now
const publishedAt = (status === 'published' && publishNow) ? new Date() : null;
// Create the post
const postId = uuidv4();
const postResult = await client.query(
`INSERT INTO blog_posts
(id, title, slug, content, excerpt, author_id, category_id, status, featured_image_path, published_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`,
[postId, title, slug, content, excerpt || null, req.user.id, categoryId || null,
status || 'draft', featuredImagePath || null, publishedAt]
);
// Add tags if provided
if (tags && Array.isArray(tags) && tags.length > 0) {
for (const tagName of tags) {
// Get or create tag
let tagId;
const tagResult = await client.query(
'SELECT id FROM tags WHERE name = $1',
[tagName]
);
if (tagResult.rows.length > 0) {
tagId = tagResult.rows[0].id;
} else {
const newTagResult = await client.query(
'INSERT INTO tags (id, name) VALUES ($1, $2) RETURNING id',
[uuidv4(), tagName]
);
tagId = newTagResult.rows[0].id;
}
// Add tag to post
await client.query(
'INSERT INTO blog_post_tags (post_id, tag_id) VALUES ($1, $2)',
[postId, tagId]
);
}
}
await client.query('COMMIT');
res.status(201).json({
message: 'Blog post created successfully',
post: postResult.rows[0]
});
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) {
next(error);
}
});
// Update blog post
router.put('/:id', async (req, res, next) => {
try {
const { id } = req.params;
const {
title, content, excerpt, categoryId,
tags, featuredImagePath, status, publishNow
} = req.body;
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Validate required fields
if (!title || !content) {
return res.status(400).json({
error: true,
message: 'Title and content are required'
});
}
// Check if post exists
const postCheck = await query(
'SELECT id, slug, status, published_at FROM blog_posts WHERE id = $1',
[id]
);
if (postCheck.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Blog post not found'
});
}
const existingPost = postCheck.rows[0];
let updatedSlug = existingPost.slug;
// Update slug if title changed
if (title) {
const newSlug = slugify(title, {
lower: true,
strict: true,
remove: /[*+~.()'"!:@]/g
});
// Only update slug if it's different
if (newSlug !== existingPost.slug) {
// Check if new slug already exists
const slugCheck = await query(
'SELECT id FROM blog_posts WHERE slug = $1 AND id != $2',
[newSlug, id]
);
if (slugCheck.rows.length === 0) {
updatedSlug = newSlug;
} else {
// Append random string to slug
const randomString = Math.random().toString(36).substring(2, 8);
updatedSlug = `${newSlug}-${randomString}`;
}
}
}
// Begin transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
// Determine published_at date
let publishedAt = existingPost.published_at;
if (status === 'published' && publishNow && !existingPost.published_at) {
publishedAt = new Date();
}
// Update the post
const postResult = await client.query(
`UPDATE blog_posts SET
title = $1,
slug = $2,
content = $3,
excerpt = $4,
category_id = $5,
status = $6,
featured_image_path = $7,
published_at = $8,
updated_at = NOW()
WHERE id = $9
RETURNING *`,
[title, updatedSlug, content, excerpt || null, categoryId || null,
status || existingPost.status, featuredImagePath, publishedAt, id]
);
// Update tags if provided
if (tags !== undefined) {
// Remove existing tags
await client.query('DELETE FROM blog_post_tags WHERE post_id = $1', [id]);
// Add new tags
if (Array.isArray(tags) && tags.length > 0) {
for (const tagName of tags) {
// Get or create tag
let tagId;
const tagResult = await client.query(
'SELECT id FROM tags WHERE name = $1',
[tagName]
);
if (tagResult.rows.length > 0) {
tagId = tagResult.rows[0].id;
} else {
const newTagResult = await client.query(
'INSERT INTO tags (id, name) VALUES ($1, $2) RETURNING id',
[uuidv4(), tagName]
);
tagId = newTagResult.rows[0].id;
}
// Add tag to post
await client.query(
'INSERT INTO blog_post_tags (post_id, tag_id) VALUES ($1, $2)',
[id, tagId]
);
}
}
}
await client.query('COMMIT');
res.json({
message: 'Blog post updated successfully',
post: postResult.rows[0]
});
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) {
next(error);
}
});
// Delete blog post
router.delete('/:id', async (req, res, next) => {
try {
const { id } = req.params;
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Check if post exists
const postCheck = await query(
'SELECT id FROM blog_posts WHERE id = $1',
[id]
);
if (postCheck.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Blog post not found'
});
}
// Delete post - cascade will handle related records
await query(
'DELETE FROM blog_posts WHERE id = $1',
[id]
);
res.json({
message: 'Blog post deleted successfully'
});
} catch (error) {
next(error);
}
});
// Upload blog post image
router.post('/:postId/images', async (req, res, next) => {
try {
const { postId } = req.params;
const { imagePath, caption, displayOrder } = req.body;
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Check if post exists
const postCheck = await query(
'SELECT id FROM blog_posts WHERE id = $1',
[postId]
);
if (postCheck.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Blog post not found'
});
}
// Check if image path is provided
if (!imagePath) {
return res.status(400).json({
error: true,
message: 'Image path is required'
});
}
// Add image to post
const result = await query(
`INSERT INTO blog_post_images (id, post_id, image_path, caption, display_order)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[uuidv4(), postId, imagePath, caption || null, displayOrder || 0]
);
res.status(201).json({
message: 'Image added to blog post',
image: result.rows[0]
});
} catch (error) {
next(error);
}
});
// Delete blog post image
router.delete('/:postId/images/:imageId', async (req, res, next) => {
try {
const { postId, imageId } = req.params;
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Check if image exists and belongs to post
const imageCheck = await query(
'SELECT id FROM blog_post_images WHERE id = $1 AND post_id = $2',
[imageId, postId]
);
if (imageCheck.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Image not found or does not belong to this post'
});
}
// Delete image
await query(
'DELETE FROM blog_post_images WHERE id = $1',
[imageId]
);
res.json({
message: 'Image deleted successfully'
});
} catch (error) {
next(error);
}
});
return router;
};