6bb4719e99
deploy to danak / build_and_deploy (push) Has been cancelled
Replace self-proxy with direct SSR baseURL, rewrite blog post pagination with async/await, pin mongoose-paginate-v2, and add API error logging. Co-authored-by: Cursor <cursoragent@cursor.com>
373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
const BlogCategory = require('../models/BlogCategory')
|
|
const BlogPost = require('../models/BlogPost')
|
|
const mongoose = require('mongoose')
|
|
const sharp = require('sharp')
|
|
const fs = require('fs')
|
|
const {body, validationResult} = require('express-validator')
|
|
const {res404, res500, checkValidations} = require('../plugins/controllersHelperFunctions')
|
|
const {_sr} = require('../plugins/serverResponses')
|
|
|
|
// variables
|
|
const coverWidth = 1920
|
|
const coverHeight = 300
|
|
const coverQuality = 100
|
|
|
|
const coverThumbWidth = 400
|
|
const coverThumbHeight = 210
|
|
const coverThumbQuality = 100
|
|
|
|
const lastSectionImageWidth = 1920
|
|
const lastSectionImageHeight = 400
|
|
const lastSectionImageQuality = 100
|
|
|
|
const blogPaginateLimit = 10
|
|
|
|
const shortDescriptionLength = 250
|
|
|
|
const videoLimit = 150;
|
|
|
|
module.exports.create = [
|
|
[
|
|
body('en_title')
|
|
.notEmpty().withMessage(_sr['fa'].required.title)
|
|
.bail()
|
|
.custom((value, {req}) => {
|
|
return BlogPost.findOne({$or: [{'locale.fa.title': value}, {'locale.en.title': value}]})
|
|
.then(post => {
|
|
if (post) return Promise.reject(_sr['fa'].duplicated.title)
|
|
else return true
|
|
})
|
|
}),
|
|
|
|
body('fa_title')
|
|
.if((value, {req}) => value)
|
|
.custom((value, {req}) => {
|
|
return BlogPost.findOne({$or: [{'locale.fa.title': value}, {'locale.en.title': value}]})
|
|
.then(post => {
|
|
if (post) return Promise.reject(_sr['fa'].duplicated.title)
|
|
else return true
|
|
})
|
|
})
|
|
],
|
|
checkValidations(validationResult),
|
|
async (req, res) => {
|
|
if (!req.files || !req.files.cover) return res.status(422).json({validation: {cover: {msg: _sr['fa'].required.cover}}})
|
|
if (!_sr.supportedImageFormats.includes(req.files.cover.mimetype.split('/')[1])) return res.status(422).json({validation: {cover: {msg: _sr['fa'].format.image}}})
|
|
|
|
const {
|
|
fa_title,
|
|
en_title
|
|
} = req.body
|
|
|
|
const data = {
|
|
locale: {
|
|
fa: {
|
|
title: fa_title
|
|
},
|
|
en: {
|
|
title: en_title
|
|
}
|
|
}
|
|
}
|
|
|
|
const image = req.files.cover
|
|
const imageName = 'blog_' + Date.now() + '.jpg'
|
|
const imageThumb = 'thumb_' + imageName
|
|
|
|
try {
|
|
const target = await sharp(image.data)
|
|
await target
|
|
.resize(coverWidth, coverHeight, {fit: 'cover'})
|
|
.withMetadata()
|
|
.toFormat('jpg')
|
|
.jpeg({quality: coverQuality})
|
|
.toFile(`./static/uploads/images/blog/${imageName}`)
|
|
|
|
await target
|
|
.resize(coverThumbWidth, coverThumbHeight, {fit: 'cover'})
|
|
.withMetadata()
|
|
.toFormat('jpg')
|
|
.jpeg({quality: coverThumbQuality})
|
|
.toFile(`./static/uploads/images/blog/thumb/${imageThumb}`)
|
|
|
|
data.cover = imageName
|
|
data.thumb = imageThumb
|
|
} catch (e) {
|
|
return res500(res, e)
|
|
}
|
|
|
|
const post = new BlogPost(data)
|
|
post.save((err, data) => {
|
|
if (err) return res500(res, err)
|
|
return res.json(data)
|
|
})
|
|
}
|
|
]
|
|
|
|
module.exports.getAll = [
|
|
async (req, res) => {
|
|
///////////// get available options
|
|
// '/api/public/blogPosts' --> gets all published posts
|
|
// '/api/public/blogPosts?getAll=true' --> gets all posts either published or not
|
|
// '/api/public/blogPosts?limit=4' --> gets only 4 published post of all categories
|
|
// '/api/public/blogPosts?category=${encodeURIComponent(categoryTitle)}&page=${pageNumber}' --> gets posts depend on the category title and in chunked data
|
|
|
|
try {
|
|
if (req.query.category) {
|
|
const filter = {published: true}
|
|
if (req.query.category !== 'all') {
|
|
const category = await BlogCategory.findOne({
|
|
$or: [
|
|
{'locale.fa.title': req.query.category},
|
|
{'locale.en.title': req.query.category}
|
|
]
|
|
})
|
|
if (!category) return res404(res, _sr['fa'].not_found.item_id)
|
|
filter.category = category._id
|
|
}
|
|
const posts = await BlogPost.paginate(filter, {
|
|
page: Number(req.query.page) || 1,
|
|
limit: blogPaginateLimit,
|
|
populate: 'category',
|
|
sort: {_id: -1}
|
|
})
|
|
return res.json(posts)
|
|
}
|
|
|
|
const query = {published: true}
|
|
if (req.query.getAll) delete query.published
|
|
const limit = Number(req.query.limit) || 0
|
|
const posts = await BlogPost.find(query)
|
|
.sort({_id: -1})
|
|
.limit(limit)
|
|
.populate('category')
|
|
return res.json(posts)
|
|
} catch (e) {
|
|
console.error('blogPost getAll error:', e)
|
|
return res500(res, e)
|
|
}
|
|
}
|
|
]
|
|
|
|
module.exports.getOne = [
|
|
(req, res) => {
|
|
const query = req.params.id
|
|
const isObjectId = mongoose.isValidObjectId(query)
|
|
const noCategory = req.query.noCategory
|
|
const filter = isObjectId ? {_id: query} : {$or: [{'locale.fa.title': query}, {'locale.en.title': query}]}
|
|
BlogPost.findOne(filter).populate(noCategory ? '' : 'category').exec((err, post) => {
|
|
if (err || !post) return res404(res, err)
|
|
else return res.json(post)
|
|
})
|
|
}
|
|
]
|
|
|
|
module.exports.update = [
|
|
[
|
|
body('en_title')
|
|
.notEmpty().withMessage(_sr['fa'].required.title)
|
|
.bail()
|
|
.custom((value, {req}) => {
|
|
return BlogPost.findOne({$or: [{'locale.fa.title': value}, {'locale.en.title': value}]})
|
|
.then(post => {
|
|
if (post && post._id.toString() !== req.params.id) return Promise.reject(_sr['fa'].duplicated.title)
|
|
else return true
|
|
})
|
|
}),
|
|
|
|
body('fa_title')
|
|
.if((value, {req}) => value)
|
|
.custom((value, {req}) => {
|
|
return BlogPost.findOne({$or: [{'locale.fa.title': value}, {'locale.en.title': value}]})
|
|
.then(post => {
|
|
if (post && post._id.toString() !== req.params.id) return Promise.reject(_sr['fa'].duplicated.title)
|
|
else return true
|
|
})
|
|
}),
|
|
|
|
body('category')
|
|
.notEmpty().withMessage(_sr['fa'].required.category)
|
|
.bail()
|
|
.custom((value, {req}) => {
|
|
if (mongoose.isValidObjectId(value)) {
|
|
return BlogCategory.findById(value)
|
|
.then(category => {
|
|
if (!category) return Promise.reject(_sr['fa'].not_found.item_id)
|
|
else return true
|
|
})
|
|
} else return Promise.reject(_sr['fa'].required.category)
|
|
})
|
|
],
|
|
checkValidations(validationResult),
|
|
async (req, res) => {
|
|
const {
|
|
fa_title,
|
|
en_title,
|
|
fa_description,
|
|
en_description,
|
|
fa_short_description,
|
|
en_short_description,
|
|
fa_last_section_image_title,
|
|
en_last_section_image_title,
|
|
fa_last_section_image_description,
|
|
en_last_section_image_description,
|
|
fa_last_section_title,
|
|
en_last_section_title,
|
|
fa_last_section_description,
|
|
en_last_section_description,
|
|
category,
|
|
published
|
|
} = req.body
|
|
|
|
const data = {
|
|
locale: {
|
|
fa: {
|
|
title: fa_title || '',
|
|
description: fa_description || '',
|
|
short_description: fa_short_description.slice(0, shortDescriptionLength) || '' + ' ... ',
|
|
last_section_image_title: fa_last_section_image_title || '',
|
|
last_section_image_description: fa_last_section_image_description || '',
|
|
last_section_title: fa_last_section_title || '',
|
|
last_section_description: fa_last_section_description || ''
|
|
},
|
|
en: {
|
|
title: en_title || '',
|
|
description: en_description || '',
|
|
short_description: en_short_description.slice(0, shortDescriptionLength) || '' + ' ... ',
|
|
last_section_image_title: en_last_section_image_title || '',
|
|
last_section_image_description: en_last_section_image_description || '',
|
|
last_section_title: en_last_section_title || '',
|
|
last_section_description: en_last_section_description || ''
|
|
}
|
|
},
|
|
category, published
|
|
}
|
|
|
|
if (req.files) {
|
|
if (req.files.cover) {
|
|
if (!_sr.supportedImageFormats.includes(req.files.cover.mimetype.split('/')[1])) return res.status(422).json({validation: {cover: {msg: _sr['fa'].format.image}}})
|
|
|
|
const image = req.files.cover
|
|
const imageName = 'blog_' + Date.now() + '.jpg'
|
|
const imageThumb = 'thumb_' + imageName
|
|
|
|
try {
|
|
const target = await sharp(image.data)
|
|
await target
|
|
.resize(coverWidth, coverHeight, {fit: 'cover'})
|
|
.withMetadata()
|
|
.withMetadata()
|
|
.toFormat('jpg')
|
|
.jpeg({quality: coverQuality})
|
|
.toFile(`./static/uploads/images/blog/${imageName}`)
|
|
|
|
await target
|
|
.resize(coverThumbWidth, coverThumbHeight, {fit: 'cover'})
|
|
.withMetadata()
|
|
.withMetadata()
|
|
.toFormat('jpg')
|
|
.jpeg({quality: coverThumbQuality})
|
|
.toFile(`./static/uploads/images/blog/thumb/${imageThumb}`)
|
|
data.cover = imageName
|
|
data.thumb = imageThumb
|
|
|
|
} catch (e) {
|
|
return res500(res, e)
|
|
}
|
|
}
|
|
|
|
if (req.files.last_section_image) {
|
|
if (!_sr.supportedImageFormats.includes(req.files.last_section_image.mimetype.split('/')[1])) return res.status(422).json({validation: {cover: {msg: _sr['fa'].format.image}}})
|
|
|
|
const image = req.files.last_section_image
|
|
const imageName = 'blog_' + Date.now() + '.jpg'
|
|
const imageThumb = 'thumb_' + imageName
|
|
|
|
try {
|
|
const target = await sharp(image.data)
|
|
await target
|
|
.resize(lastSectionImageWidth, lastSectionImageHeight, {fit: 'cover'})
|
|
.withMetadata()
|
|
.toFormat('jpg')
|
|
.jpeg({quality: lastSectionImageQuality})
|
|
.toFile(`./static/uploads/images/blog/${imageName}`)
|
|
|
|
data.last_section_image = imageName
|
|
|
|
} catch (e) {
|
|
return res500(res, e)
|
|
}
|
|
}
|
|
|
|
if (req.files.video) {
|
|
const file = req?.files?.video
|
|
if (file.mimetype.split('/')[1] !== 'mp4') return res.status(422).json({validation: {video: {msg: _sr['fa'].format.video}}})
|
|
|
|
///// start convert byte to mb
|
|
const mb = file.size / (1024 * 1024);
|
|
|
|
///// check limit size of video
|
|
if (mb > videoLimit) {
|
|
return res.status(422).json({
|
|
validation: {file: {msg: `${_sr['fa'].max_char.data_size} 150 مگابایت`}}
|
|
});
|
|
}
|
|
|
|
const videoName = 'blog_' + Date.now() + '.mp4'
|
|
const uploadPath = `./static/uploads/videos/blog/${videoName}`;
|
|
|
|
try {
|
|
await file.mv(uploadPath);
|
|
data.video = videoName
|
|
} catch (e) {
|
|
return res500(res, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
BlogPost.findByIdAndUpdate(req.params.id, data, (err, oldData) => {
|
|
if (err || !oldData) return res404(res, err)
|
|
if (req.files && req.files.cover) {
|
|
fs.unlink(`./static/${oldData.cover}`, (err) => {
|
|
if (err) console.log(err)
|
|
})
|
|
fs.unlink(`./static/${oldData.thumb}`, (err) => {
|
|
if (err) console.log(err)
|
|
})
|
|
}
|
|
if (req.files && req.files.last_section_image && oldData.last_section_image) {
|
|
fs.unlink(`./static/${oldData.last_section_image}`, (err) => {
|
|
if (err) console.log(err)
|
|
})
|
|
}
|
|
if (req.files && req.files.video && oldData.video) {
|
|
fs.unlink(`./static/${oldData.video}`, (err) => {
|
|
if (err) console.log(err)
|
|
})
|
|
}
|
|
return res.json({message: _sr['fa'].response.success_save})
|
|
})
|
|
}
|
|
]
|
|
|
|
module.exports.delete = [
|
|
(req, res) => {
|
|
BlogPost.findByIdAndRemove(req.params.id, (err, data) => {
|
|
if (err || !data) return res404(res, err)
|
|
fs.unlink(`./static/${data.cover}`, err => {
|
|
if (err) console.log(err)
|
|
})
|
|
fs.unlink(`./static/${data.thumb}`, err => {
|
|
if (err) console.log(err)
|
|
})
|
|
if (data.last_section_image) fs.unlink(`./static/${data.last_section_image}`, err => {
|
|
if (err) console.log(err)
|
|
})
|
|
if (data.video) fs.unlink(`./static/${data.video}`, err => {
|
|
if (err) console.log(err)
|
|
})
|
|
return res.json({message: _sr['fa'].response.success_remove})
|
|
})
|
|
}
|
|
]
|