Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into fea/bao-03022026-Admin-Management-Service

This commit is contained in:
nguyenvanbao
2026-02-03 16:21:56 +07:00
29 changed files with 1855 additions and 2155 deletions

View File

@@ -0,0 +1,231 @@
const BlogCategory = require('../models/blogCategory');
// -------------------- Admin Controllers --------------------
// Display category management page
exports.index = async (req, res) => {
try {
const categories = await BlogCategory.find()
.sort({ name: 1 })
.lean();
res.render('admin/blog/categories/index', {
layout: 'layouts/main',
title: 'Blog Categories',
categories,
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Category index error:', err);
req.flash('error_msg', 'Error loading categories');
res.redirect('/admin/dashboard');
}
};
// Show create category form
exports.create = async (req, res) => {
try {
res.render('admin/blog/categories/create', {
layout: 'layouts/main',
title: 'Create New Category',
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Category create form error:', err);
req.flash('error_msg', 'Error loading create form');
res.redirect('/admin/blog/categories');
}
};
// Store new category
exports.store = async (req, res) => {
try {
const {
name,
description,
isActive
} = req.body;
// Generate slug
const slug = name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
// Check if slug exists
const existingCategory = await BlogCategory.findOne({ slug });
if (existingCategory) {
req.flash('error_msg', 'A category with this name already exists');
return res.redirect('/admin/blog/categories/create');
}
// Create category data
const categoryData = {
name,
slug,
description,
isActive: isActive === 'on'
};
// Create category
const category = new BlogCategory(categoryData);
await category.save();
req.flash('success_msg', 'Category created successfully');
res.redirect('/admin/blog/categories');
} catch (err) {
console.error('Category store error:', err);
req.flash('error_msg', 'Error creating category');
res.redirect('/admin/blog/categories/create');
}
};
// Show edit category form
exports.edit = async (req, res) => {
try {
const category = await BlogCategory.findById(req.params.id);
if (!category) {
req.flash('error_msg', 'Category not found');
return res.redirect('/admin/blog/categories');
}
res.render('admin/blog/categories/edit', {
layout: 'layouts/main',
title: 'Edit Category',
category,
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Category edit form error:', err);
req.flash('error_msg', 'Error loading category');
res.redirect('/admin/blog/categories');
}
};
// Update category
exports.update = async (req, res) => {
try {
const category = await BlogCategory.findById(req.params.id);
if (!category) {
req.flash('error_msg', 'Category not found');
return res.redirect('/admin/blog/categories');
}
const {
name,
description,
isActive
} = req.body;
// Update category data
category.name = name;
category.description = description;
category.isActive = isActive === 'on';
// Generate new slug if name changed
const newSlug = name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
if (newSlug !== category.slug) {
const existingCategory = await BlogCategory.findOne({
slug: newSlug,
_id: { $ne: category._id }
});
if (existingCategory) {
req.flash('error_msg', 'A category with this name already exists');
return res.redirect(`/admin/blog/categories/${category._id}/edit`);
}
category.slug = newSlug;
}
await category.save();
req.flash('success_msg', 'Category updated successfully');
res.redirect('/admin/blog/categories');
} catch (err) {
console.error('Category update error:', err);
req.flash('error_msg', 'Error updating category');
res.redirect(`/admin/blog/categories/${req.params.id}/edit`);
}
};
// Delete category
exports.destroy = async (req, res) => {
try {
const category = await BlogCategory.findById(req.params.id);
if (!category) {
req.flash('error_msg', 'Category not found');
return res.redirect('/admin/blog/categories');
}
// Check if category has posts
const Blog = require('../models/blog');
const postCount = await Blog.countDocuments({ category: { $in: [category.name] } });
if (postCount > 0) {
req.flash('error_msg', 'Cannot delete category that has blog posts');
return res.redirect('/admin/blog/categories');
}
await BlogCategory.findByIdAndDelete(req.params.id);
req.flash('success_msg', 'Category deleted successfully');
res.redirect('/admin/blog/categories');
} catch (err) {
console.error('Category delete error:', err);
req.flash('error_msg', 'Error deleting category');
res.redirect('/admin/blog/categories');
}
};
// -------------------- Public API Controllers --------------------
// Get all active categories
exports.api = async (req, res) => {
try {
const categories = await BlogCategory.getActive();
// Update post counts
for (const category of categories) {
await category.updatePostCount();
}
res.json(categories);
} catch (err) {
console.error('Categories API error:', err);
res.status(500).json({ error: 'Error loading categories' });
}
};
// Get category by slug
exports.apiShow = async (req, res) => {
try {
const category = await BlogCategory.findOne({
slug: req.params.slug,
isActive: true
}).lean();
if (!category) {
return res.status(404).json({ error: 'Category not found' });
}
res.json(category);
} catch (err) {
console.error('Category show API error:', err);
res.status(500).json({ error: 'Error loading category' });
}
};
module.exports = exports;

View File

@@ -0,0 +1,554 @@
const Blog = require('../models/blog');
const BlogCategory = require('../models/blogCategory');
const BlogTag = require('../models/blogTag');
const BlogComment = require('../models/blogComment');
const RecentPost = require('../models/recentPost');
const { addBaseUrlToImages } = require('../utils/imageHelper');
// -------------------- Helper Functions --------------------
// Generate slug from title
const generateSlug = (title) => {
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
};
// Update category post counts
const updateCategoryPostCounts = async () => {
const categories = await BlogCategory.find();
for (const category of categories) {
await category.updatePostCount();
}
};
// Update tag post counts
const updateTagPostCounts = async () => {
const tags = await BlogTag.find();
for (const tag of tags) {
await tag.updatePostCount();
}
};
// -------------------- Admin Controllers --------------------
// Display blog management page
exports.index = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
// Build filter
const filter = {};
if (req.query.status) {
filter.status = req.query.status;
}
if (req.query.category) {
filter.category = req.query.category;
}
if (req.query.search) {
filter.$or = [
{ title: { $regex: req.query.search, $options: 'i' } },
{ excerpt: { $regex: req.query.search, $options: 'i' } }
];
}
// Get blogs with pagination
const blogs = await Blog.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean();
const totalBlogs = await Blog.countDocuments(filter);
const totalPages = Math.ceil(totalBlogs / limit);
// Get categories for filter
const categories = await BlogCategory.getActive();
res.render('admin/blog/index', {
layout: 'layouts/main',
title: 'Blog Management',
blogs,
categories,
pagination: {
current: page,
total: totalPages,
limit,
totalItems: totalBlogs
},
query: req.query,
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Blog index error:', err);
req.flash('error_msg', 'Error loading blogs');
res.redirect('/admin/dashboard');
}
};
// Show create blog form
exports.create = async (req, res) => {
try {
const categories = await BlogCategory.getActive();
const tags = await BlogTag.getActive();
res.render('admin/blog/create', {
layout: 'layouts/main',
title: 'Create New Blog Post',
categories,
tags,
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Blog create form error:', err);
req.flash('error_msg', 'Error loading create form');
res.redirect('/admin/blog');
}
};
// Store new blog
exports.store = async (req, res) => {
try {
const {
title,
excerpt,
content,
category,
tags,
status,
isFeatured,
author,
galleryImages
} = req.body;
// Generate slug
const slug = generateSlug(title);
// Check if slug exists
const existingBlog = await Blog.findOne({ slug });
if (existingBlog) {
req.flash('error_msg', 'A blog post with this title already exists');
return res.redirect('/admin/blog/create');
}
// Create blog data
const blogData = {
title,
slug,
excerpt,
content,
category: category ? (Array.isArray(category) ? category : [category]) : [], // Array categories
tags: tags ? (Array.isArray(tags) ? tags : [tags]) : [],
status: status || 'published',
isFeatured: isFeatured === 'on',
author: author || 'Admin',
galleryImages: galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : []
};
// Handle featured image
if (req.file) {
blogData.featuredImage = `/uploads/blog/${req.file.filename}`;
}
// Create blog
const blog = new Blog(blogData);
await blog.save();
// Update counts
await updateCategoryPostCounts();
await updateTagPostCounts();
await RecentPost.syncFromBlogs();
req.flash('success_msg', 'Blog post created successfully');
res.redirect('/admin/blog');
} catch (err) {
console.error('Blog store error:', err);
req.flash('error_msg', 'Error creating blog post');
res.redirect('/admin/blog/create');
}
};
// Show edit blog form
exports.edit = async (req, res) => {
try {
const blog = await Blog.findById(req.params.id);
if (!blog) {
req.flash('error_msg', 'Blog post not found');
return res.redirect('/admin/blog');
}
const categories = await BlogCategory.getActive();
const tags = await BlogTag.getActive();
res.render('admin/blog/edit', {
layout: 'layouts/main',
title: 'Edit Blog Post',
blog,
categories,
tags,
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Blog edit form error:', err);
req.flash('error_msg', 'Error loading blog post');
res.redirect('/admin/blog');
}
};
// Update blog
exports.update = async (req, res) => {
try {
const blog = await Blog.findById(req.params.id);
if (!blog) {
req.flash('error_msg', 'Blog post not found');
return res.redirect('/admin/blog');
}
const {
title,
excerpt,
content,
category,
tags,
status,
isFeatured,
author,
galleryImages
} = req.body;
// Update blog data
blog.title = title;
blog.excerpt = excerpt;
blog.content = content;
blog.category = category ? (Array.isArray(category) ? category : [category]) : []; // Array categories
blog.tags = tags ? (Array.isArray(tags) ? tags : [tags]) : [];
blog.status = status || 'published';
blog.isFeatured = isFeatured === 'on';
blog.author = author || 'Admin';
blog.galleryImages = galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [];
// Handle featured image
if (req.file) {
blog.featuredImage = `/uploads/blog/${req.file.filename}`;
}
// Generate new slug if title changed
const newSlug = generateSlug(title);
if (newSlug !== blog.slug) {
const existingBlog = await Blog.findOne({ slug: newSlug, _id: { $ne: blog._id } });
if (existingBlog) {
req.flash('error_msg', 'A blog post with this title already exists');
return res.redirect(`/admin/blog/${blog._id}/edit`);
}
blog.slug = newSlug;
}
await blog.save();
// Update counts
await updateCategoryPostCounts();
await updateTagPostCounts();
await RecentPost.syncFromBlogs();
req.flash('success_msg', 'Blog post updated successfully');
res.redirect('/admin/blog');
} catch (err) {
console.error('Blog update error:', err);
req.flash('error_msg', 'Error updating blog post');
res.redirect(`/admin/blog/${req.params.id}/edit`);
}
};
// Delete blog
exports.destroy = async (req, res) => {
try {
const blog = await Blog.findById(req.params.id);
if (!blog) {
req.flash('error_msg', 'Blog post not found');
return res.redirect('/admin/blog');
}
await Blog.findByIdAndDelete(req.params.id);
// Update counts
await updateCategoryPostCounts();
await updateTagPostCounts();
await RecentPost.syncFromBlogs();
req.flash('success_msg', 'Blog post deleted successfully');
res.redirect('/admin/blog');
} catch (err) {
console.error('Blog delete error:', err);
req.flash('error_msg', 'Error deleting blog post');
res.redirect('/admin/blog');
}
};
// -------------------- Public API Controllers --------------------
// Get all published blogs for frontend
exports.api = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
// Build filter
const filter = { status: 'published' };
if (req.query.category) {
filter.category = { $in: [req.query.category] }; // Tìm trong array categories
}
if (req.query.tag) {
filter.tags = { $in: [req.query.tag] }; // Tìm trong array tags
}
if (req.query.search) {
filter.$or = [
{ title: { $regex: req.query.search, $options: 'i' } },
{ excerpt: { $regex: req.query.search, $options: 'i' } }
];
}
// Get blogs
const blogs = await Blog.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean();
const totalBlogs = await Blog.countDocuments(filter);
// Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
res.json({
success: true,
message: 'Blogs fetched successfully',
data: {
blogs: processedBlogs,
pagination: {
current: page,
total: Math.ceil(totalBlogs / limit),
limit,
totalItems: totalBlogs
}
}
});
} catch (err) {
console.error('Blog API error:', err);
res.status(500).json({
success: false,
message: 'Error loading blogs',
error: err.message || 'Error loading blogs'
});
}
};
// Get single blog by slug
exports.apiShow = async (req, res) => {
try {
const blog = await Blog.findOne({
slug: req.params.slug,
status: 'published'
}).lean();
if (!blog) {
return res.status(404).json({
success: false,
message: 'Blog post not found'
});
}
// Get comments for this post
const comments = await BlogComment.getApprovedByPost(blog._id);
// Add comments to blog
blog.comments = comments;
// Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processedBlog = addBaseUrlToImages(blog, baseUrl);
res.json({
success: true,
message: 'Blog post fetched successfully',
data: processedBlog
});
} catch (err) {
console.error('Blog show API error:', err);
res.status(500).json({
success: false,
message: 'Error loading blog post',
error: err.message || 'Error loading blog post'
});
}
};
// Get featured blogs
exports.apiFeatured = async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 3;
const blogs = await Blog.getFeatured()
.limit(limit)
.lean();
// Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
res.json({
success: true,
message: 'Featured blogs fetched successfully',
data: processedBlogs
});
} catch (err) {
console.error('Featured blogs API error:', err);
res.status(500).json({
success: false,
message: 'Error loading featured blogs',
error: err.message || 'Error loading featured blogs'
});
}
};
// Get recent blogs
exports.apiRecent = async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 5;
// Try to get from RecentPost first
let recentPosts = await RecentPost.getRecent(limit);
// If no recent posts, sync from blogs
if (recentPosts.length === 0) {
await RecentPost.syncFromBlogs(limit);
recentPosts = await RecentPost.getRecent(limit);
}
// Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processedPosts = recentPosts.map(post => addBaseUrlToImages(post, baseUrl));
res.json({
success: true,
message: 'Recent blogs fetched successfully',
data: processedPosts
});
} catch (err) {
console.error('Recent blogs API error:', err);
res.status(500).json({
success: false,
message: 'Error loading recent blogs',
error: err.message || 'Error loading recent blogs'
});
}
};
// Get categories of a specific blog post
exports.apiCategories = async (req, res) => {
try {
const mongoose = require('mongoose');
let query;
// Check if it's a valid ObjectId
if (mongoose.Types.ObjectId.isValid(req.params.id)) {
query = { _id: req.params.id };
} else {
query = { slug: req.params.id };
}
query.status = 'published';
const blog = await Blog.findOne(query).lean();
if (!blog) {
return res.status(404).json({
success: false,
message: 'Blog post not found'
});
}
// Get category details
const BlogCategory = require('../models/blogCategory');
const categories = await BlogCategory.find({
name: { $in: blog.category },
isActive: true
}).lean();
res.json({
success: true,
message: 'Blog categories fetched successfully',
data: categories
});
} catch (err) {
console.error('Blog categories API error:', err);
res.status(500).json({
success: false,
message: 'Error loading blog categories',
error: err.message || 'Error loading blog categories'
});
}
};
// Get tags of a specific blog post
exports.apiTags = async (req, res) => {
try {
const mongoose = require('mongoose');
let query;
// Check if it's a valid ObjectId
if (mongoose.Types.ObjectId.isValid(req.params.id)) {
query = { _id: req.params.id };
} else {
query = { slug: req.params.id };
}
query.status = 'published';
const blog = await Blog.findOne(query).lean();
if (!blog) {
return res.status(404).json({
success: false,
message: 'Blog post not found'
});
}
// Get tag details
const BlogTag = require('../models/blogTag');
const tags = await BlogTag.find({
name: { $in: blog.tags },
isActive: true
}).lean();
res.json({
success: true,
message: 'Blog tags fetched successfully',
data: tags
});
} catch (err) {
console.error('Blog tags API error:', err);
res.status(500).json({
success: false,
message: 'Error loading blog tags',
error: err.message || 'Error loading blog tags'
});
}
};
module.exports = exports;

View File

@@ -0,0 +1,239 @@
const BlogTag = require('../models/blogTag');
// -------------------- Admin Controllers --------------------
// Display tag management page
exports.index = async (req, res) => {
try {
const tags = await BlogTag.find()
.sort({ name: 1 })
.lean();
res.render('admin/blog/tags/index', {
layout: 'layouts/main',
title: 'Blog Tags',
tags,
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Tag index error:', err);
req.flash('error_msg', 'Error loading tags');
res.redirect('/admin/dashboard');
}
};
// Show create tag form
exports.create = async (req, res) => {
try {
res.render('admin/blog/tags/create', {
layout: 'layouts/main',
title: 'Create New Tag',
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Tag create form error:', err);
req.flash('error_msg', 'Error loading create form');
res.redirect('/admin/blog/tags');
}
};
// Store new tag
exports.store = async (req, res) => {
try {
const {
name,
isActive
} = req.body;
// Generate slug
const slug = name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
// Check if slug exists
const existingTag = await BlogTag.findOne({ slug });
if (existingTag) {
req.flash('error_msg', 'A tag with this name already exists');
return res.redirect('/admin/blog/tags/create');
}
// Create tag data
const tagData = {
name,
slug,
isActive: isActive === 'on'
};
// Create tag
const tag = new BlogTag(tagData);
await tag.save();
req.flash('success_msg', 'Tag created successfully');
res.redirect('/admin/blog/tags');
} catch (err) {
console.error('Tag store error:', err);
req.flash('error_msg', 'Error creating tag');
res.redirect('/admin/blog/tags/create');
}
};
// Show edit tag form
exports.edit = async (req, res) => {
try {
const tag = await BlogTag.findById(req.params.id);
if (!tag) {
req.flash('error_msg', 'Tag not found');
return res.redirect('/admin/blog/tags');
}
res.render('admin/blog/tags/edit', {
layout: 'layouts/main',
title: 'Edit Tag',
tag,
currentPath: req.path,
user: req.session.user
});
} catch (err) {
console.error('Tag edit form error:', err);
req.flash('error_msg', 'Error loading tag');
res.redirect('/admin/blog/tags');
}
};
// Update tag
exports.update = async (req, res) => {
try {
const tag = await BlogTag.findById(req.params.id);
if (!tag) {
req.flash('error_msg', 'Tag not found');
return res.redirect('/admin/blog/tags');
}
const {
name,
isActive
} = req.body;
// Update tag data
tag.name = name;
tag.isActive = isActive === 'on';
// Generate new slug if name changed
const newSlug = name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
if (newSlug !== tag.slug) {
const existingTag = await BlogTag.findOne({
slug: newSlug,
_id: { $ne: tag._id }
});
if (existingTag) {
req.flash('error_msg', 'A tag with this name already exists');
return res.redirect(`/admin/blog/tags/${tag._id}/edit`);
}
tag.slug = newSlug;
}
await tag.save();
req.flash('success_msg', 'Tag updated successfully');
res.redirect('/admin/blog/tags');
} catch (err) {
console.error('Tag update error:', err);
req.flash('error_msg', 'Error updating tag');
res.redirect(`/admin/blog/tags/${req.params.id}/edit`);
}
};
// Delete tag
exports.destroy = async (req, res) => {
try {
const tag = await BlogTag.findById(req.params.id);
if (!tag) {
req.flash('error_msg', 'Tag not found');
return res.redirect('/admin/blog/tags');
}
// Check if tag has posts
const Blog = require('../models/blog');
const postCount = await Blog.countDocuments({ tags: { $in: [tag.name] } });
if (postCount > 0) {
req.flash('error_msg', 'Cannot delete tag that is used in blog posts');
return res.redirect('/admin/blog/tags');
}
await BlogTag.findByIdAndDelete(req.params.id);
req.flash('success_msg', 'Tag deleted successfully');
res.redirect('/admin/blog/tags');
} catch (err) {
console.error('Tag delete error:', err);
req.flash('error_msg', 'Error deleting tag');
res.redirect('/admin/blog/tags');
}
};
// -------------------- Public API Controllers --------------------
// Get all active tags
exports.api = async (req, res) => {
try {
const tags = await BlogTag.getActive();
// Update post counts
for (const tag of tags) {
await tag.updatePostCount();
}
res.json(tags);
} catch (err) {
console.error('Tags API error:', err);
res.status(500).json({ error: 'Error loading tags' });
}
};
// Get popular tags
exports.apiPopular = async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
const tags = await BlogTag.getPopular(limit);
res.json(tags);
} catch (err) {
console.error('Popular tags API error:', err);
res.status(500).json({ error: 'Error loading popular tags' });
}
};
// Get tag by slug
exports.apiShow = async (req, res) => {
try {
const tag = await BlogTag.findOne({
slug: req.params.slug,
isActive: true
}).lean();
if (!tag) {
return res.status(404).json({ error: 'Tag not found' });
}
res.json(tag);
} catch (err) {
console.error('Tag show API error:', err);
res.status(500).json({ error: 'Error loading tag' });
}
};
module.exports = exports;