2018-02-05 23:20:44 +10:00
|
|
|
const express = require('express');
|
2018-02-06 04:20:30 +10:00
|
|
|
const common = require('../lib/common');
|
2019-06-15 14:46:08 +10:00
|
|
|
const { restrict, checkAccess } = require('../lib/auth');
|
2019-06-17 20:21:45 +10:00
|
|
|
const { indexProducts } = require('../lib/indexing');
|
|
|
|
const { validateJson } = require('../lib/schema');
|
2018-02-05 23:20:44 +10:00
|
|
|
const colors = require('colors');
|
|
|
|
const rimraf = require('rimraf');
|
|
|
|
const fs = require('fs');
|
|
|
|
const path = require('path');
|
|
|
|
const router = express.Router();
|
|
|
|
|
2020-03-19 16:32:52 +10:00
|
|
|
router.get('/admin/products/:page?', restrict, async (req, res, next) => {
|
|
|
|
let pageNum = 1;
|
|
|
|
if(req.params.page){
|
|
|
|
pageNum = req.params.page;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get our paginated data
|
|
|
|
const products = await common.paginateData(false, req, pageNum, 'products', {}, { productAddedDate: -1 });
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
res.render('products', {
|
|
|
|
title: 'Cart',
|
2020-03-19 16:32:52 +10:00
|
|
|
results: products.data,
|
|
|
|
totalItemCount: products.totalItems,
|
|
|
|
pageNum,
|
|
|
|
paginateUrl: 'admin/products',
|
2019-12-13 21:06:25 +10:00
|
|
|
resultType: 'top',
|
2019-10-26 11:08:53 +10:00
|
|
|
session: req.session,
|
|
|
|
admin: true,
|
|
|
|
config: req.app.config,
|
|
|
|
message: common.clearSessionValue(req.session, 'message'),
|
|
|
|
messageType: common.clearSessionValue(req.session, 'messageType'),
|
|
|
|
helpers: req.handlebars.helpers
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-11-09 12:59:20 +10:00
|
|
|
router.get('/admin/products/filter/:search', restrict, async (req, res, next) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
2019-07-12 18:06:34 +10:00
|
|
|
const searchTerm = req.params.search;
|
|
|
|
const productsIndex = req.app.productsIndex;
|
2018-02-05 23:20:44 +10:00
|
|
|
|
2019-07-12 18:06:34 +10:00
|
|
|
const lunrIdArray = [];
|
2018-02-05 23:20:44 +10:00
|
|
|
productsIndex.search(searchTerm).forEach((id) => {
|
|
|
|
lunrIdArray.push(common.getId(id.ref));
|
|
|
|
});
|
|
|
|
|
|
|
|
// we search on the lunr indexes
|
2019-10-26 11:08:53 +10:00
|
|
|
const results = await db.products.find({ _id: { $in: lunrIdArray } }).toArray();
|
2019-11-09 12:59:20 +10:00
|
|
|
|
|
|
|
if(req.apiAuthenticated){
|
|
|
|
res.status(200).json(results);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
res.render('products', {
|
|
|
|
title: 'Results',
|
|
|
|
results: results,
|
2019-12-13 21:06:25 +10:00
|
|
|
resultType: 'filtered',
|
2019-10-26 11:08:53 +10:00
|
|
|
admin: true,
|
|
|
|
config: req.app.config,
|
|
|
|
session: req.session,
|
|
|
|
searchTerm: searchTerm,
|
|
|
|
message: common.clearSessionValue(req.session, 'message'),
|
|
|
|
messageType: common.clearSessionValue(req.session, 'messageType'),
|
|
|
|
helpers: req.handlebars.helpers
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// insert form
|
2019-06-15 14:46:08 +10:00
|
|
|
router.get('/admin/product/new', restrict, checkAccess, (req, res) => {
|
2020-01-01 19:26:21 +10:00
|
|
|
res.render('product-new', {
|
2018-02-05 23:20:44 +10:00
|
|
|
title: 'New product',
|
|
|
|
session: req.session,
|
|
|
|
productTitle: common.clearSessionValue(req.session, 'productTitle'),
|
|
|
|
productDescription: common.clearSessionValue(req.session, 'productDescription'),
|
|
|
|
productPrice: common.clearSessionValue(req.session, 'productPrice'),
|
|
|
|
productPermalink: common.clearSessionValue(req.session, 'productPermalink'),
|
|
|
|
message: common.clearSessionValue(req.session, 'message'),
|
|
|
|
messageType: common.clearSessionValue(req.session, 'messageType'),
|
|
|
|
editor: true,
|
|
|
|
admin: true,
|
|
|
|
helpers: req.handlebars.helpers,
|
2018-02-23 03:41:24 +10:00
|
|
|
config: req.app.config
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// insert new product form action
|
2019-10-26 11:08:53 +10:00
|
|
|
router.post('/admin/product/insert', restrict, checkAccess, async (req, res) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
|
|
|
|
2019-06-17 20:21:45 +10:00
|
|
|
// Process supplied options
|
|
|
|
let productOptions = req.body.productOptions;
|
|
|
|
if(productOptions && typeof productOptions !== 'object'){
|
|
|
|
try{
|
|
|
|
productOptions = JSON.parse(req.body.productOptions);
|
|
|
|
}catch(ex){
|
|
|
|
console.log('Failure to parse options');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-12 18:06:34 +10:00
|
|
|
const doc = {
|
2019-06-17 20:21:45 +10:00
|
|
|
productPermalink: req.body.productPermalink,
|
|
|
|
productTitle: common.cleanHtml(req.body.productTitle),
|
2020-01-08 18:28:29 +10:00
|
|
|
productPrice: req.body.productPrice,
|
2019-06-17 20:21:45 +10:00
|
|
|
productDescription: common.cleanHtml(req.body.productDescription),
|
|
|
|
productPublished: common.convertBool(req.body.productPublished),
|
|
|
|
productTags: req.body.productTags,
|
|
|
|
productOptions: productOptions || null,
|
|
|
|
productComment: common.checkboxBool(req.body.productComment),
|
2019-02-09 20:46:41 +10:00
|
|
|
productAddedDate: new Date(),
|
2019-06-17 20:21:45 +10:00
|
|
|
productStock: common.safeParseInt(req.body.productStock) || null
|
2018-02-05 23:20:44 +10:00
|
|
|
};
|
|
|
|
|
2019-06-17 20:21:45 +10:00
|
|
|
// Validate the body again schema
|
2019-11-16 12:23:16 +10:00
|
|
|
const schemaValidate = validateJson('newProduct', doc);
|
|
|
|
if(!schemaValidate.result){
|
|
|
|
console.log('schemaValidate errors', schemaValidate.errors);
|
2019-12-16 13:40:56 +10:00
|
|
|
res.status(400).json(schemaValidate.errors);
|
2019-06-17 20:21:45 +10:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-10-26 11:20:58 +10:00
|
|
|
// Check permalink doesn't already exist
|
2019-10-29 18:26:30 +10:00
|
|
|
const product = await db.products.countDocuments({ productPermalink: req.body.productPermalink });
|
2019-10-26 11:08:53 +10:00
|
|
|
if(product > 0 && req.body.productPermalink !== ''){
|
2019-12-16 13:40:56 +10:00
|
|
|
res.status(400).json({ message: 'Permalink already exists. Pick a new one.' });
|
2019-10-26 11:08:53 +10:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try{
|
2019-10-26 11:20:58 +10:00
|
|
|
const newDoc = await db.products.insertOne(doc);
|
2019-10-26 11:08:53 +10:00
|
|
|
// get the new ID
|
2019-10-26 11:20:58 +10:00
|
|
|
const newId = newDoc.insertedId;
|
2019-10-26 11:08:53 +10:00
|
|
|
|
|
|
|
// add to lunr index
|
|
|
|
indexProducts(req.app)
|
|
|
|
.then(() => {
|
2019-12-16 13:40:56 +10:00
|
|
|
res.status(200).json({
|
|
|
|
message: 'New product successfully created',
|
|
|
|
productId: newId
|
|
|
|
});
|
2019-10-26 11:08:53 +10:00
|
|
|
});
|
|
|
|
}catch(ex){
|
|
|
|
console.log(colors.red('Error inserting document: ' + ex));
|
2019-12-16 13:40:56 +10:00
|
|
|
res.status(400).json({ message: 'Error inserting document' });
|
2019-10-26 11:08:53 +10:00
|
|
|
}
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
// render the editor
|
2019-10-26 11:08:53 +10:00
|
|
|
router.get('/admin/product/edit/:id', restrict, checkAccess, async (req, res) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
const images = await common.getImages(req.params.id, req, res);
|
|
|
|
const product = await db.products.findOne({ _id: common.getId(req.params.id) });
|
|
|
|
if(!product){
|
2019-11-09 12:59:20 +10:00
|
|
|
// If API request, return json
|
|
|
|
if(req.apiAuthenticated){
|
|
|
|
res.status(400).json({ message: 'Product not found' });
|
|
|
|
return;
|
|
|
|
}
|
2019-10-26 11:08:53 +10:00
|
|
|
req.session.message = 'Product not found';
|
|
|
|
req.session.messageType = 'danger';
|
|
|
|
res.redirect('/admin/products');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let options = {};
|
|
|
|
if(product.productOptions){
|
|
|
|
options = product.productOptions;
|
2019-12-16 12:30:30 +10:00
|
|
|
if(typeof product.productOptions !== 'object'){
|
|
|
|
options = JSON.parse(product.productOptions);
|
|
|
|
}
|
2019-10-26 11:08:53 +10:00
|
|
|
}
|
2018-02-05 23:20:44 +10:00
|
|
|
|
2019-11-09 12:59:20 +10:00
|
|
|
// If API request, return json
|
|
|
|
if(req.apiAuthenticated){
|
|
|
|
res.status(200).json(product);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-01-01 19:26:21 +10:00
|
|
|
res.render('product-edit', {
|
2019-10-26 11:08:53 +10:00
|
|
|
title: 'Edit product',
|
|
|
|
result: product,
|
|
|
|
images: images,
|
|
|
|
options: options,
|
|
|
|
admin: true,
|
|
|
|
session: req.session,
|
|
|
|
message: common.clearSessionValue(req.session, 'message'),
|
|
|
|
messageType: common.clearSessionValue(req.session, 'messageType'),
|
|
|
|
config: req.app.config,
|
|
|
|
editor: true,
|
|
|
|
helpers: req.handlebars.helpers
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
// Remove option from product
|
|
|
|
router.post('/admin/product/removeoption', restrict, checkAccess, async (req, res) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
2019-10-26 11:08:53 +10:00
|
|
|
const product = await db.products.findOne({ _id: common.getId(req.body.productId) });
|
|
|
|
if(product && product.productOptions){
|
|
|
|
const opts = product.productOptions;
|
|
|
|
delete opts[req.body.optName];
|
2018-02-05 23:20:44 +10:00
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
try{
|
2019-12-16 12:30:30 +10:00
|
|
|
const updateOption = await db.products.findOneAndUpdate({ _id: common.getId(req.body.productId) }, { $set: { productOptions: opts } });
|
|
|
|
if(updateOption.ok === 1){
|
2019-10-26 11:08:53 +10:00
|
|
|
res.status(200).json({ message: 'Option successfully removed' });
|
2019-06-20 13:14:04 +10:00
|
|
|
return;
|
|
|
|
}
|
2019-10-26 11:08:53 +10:00
|
|
|
res.status(400).json({ message: 'Failed to remove option. Please try again.' });
|
|
|
|
return;
|
|
|
|
}catch(ex){
|
|
|
|
res.status(400).json({ message: 'Failed to remove option. Please try again.' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
res.status(400).json({ message: 'Product not found. Try saving before removing.' });
|
|
|
|
});
|
2019-06-20 13:14:04 +10:00
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
// Update an existing product form action
|
|
|
|
router.post('/admin/product/update', restrict, checkAccess, async (req, res) => {
|
|
|
|
const db = req.app.db;
|
|
|
|
|
|
|
|
const product = await db.products.findOne({ _id: common.getId(req.body.productId) });
|
|
|
|
|
|
|
|
if(!product){
|
2019-12-16 12:30:30 +10:00
|
|
|
res.status(400).json({ message: 'Failed to update product' });
|
2019-10-26 11:08:53 +10:00
|
|
|
return;
|
|
|
|
}
|
2019-10-29 18:26:30 +10:00
|
|
|
const count = await db.products.countDocuments({ productPermalink: req.body.productPermalink, _id: { $ne: common.getId(product._id) } });
|
2019-10-26 11:08:53 +10:00
|
|
|
if(count > 0 && req.body.productPermalink !== ''){
|
2019-12-16 12:30:30 +10:00
|
|
|
res.status(400).json({ message: 'Permalink already exists. Pick a new one.' });
|
2019-10-26 11:08:53 +10:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const images = await common.getImages(req.body.productId, req, res);
|
|
|
|
// Process supplied options
|
|
|
|
let productOptions = req.body.productOptions;
|
|
|
|
if(productOptions && typeof productOptions !== 'object'){
|
|
|
|
try{
|
|
|
|
productOptions = JSON.parse(req.body.productOptions);
|
|
|
|
}catch(ex){
|
|
|
|
console.log('Failure to parse options');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const productDoc = {
|
|
|
|
productId: req.body.productId,
|
|
|
|
productPermalink: req.body.productPermalink,
|
|
|
|
productTitle: common.cleanHtml(req.body.productTitle),
|
2020-01-08 18:28:29 +10:00
|
|
|
productPrice: req.body.productPrice,
|
2019-10-26 11:08:53 +10:00
|
|
|
productDescription: common.cleanHtml(req.body.productDescription),
|
|
|
|
productPublished: common.convertBool(req.body.productPublished),
|
|
|
|
productTags: req.body.productTags,
|
|
|
|
productOptions: productOptions || null,
|
|
|
|
productComment: common.checkboxBool(req.body.productComment),
|
|
|
|
productStock: common.safeParseInt(req.body.productStock) || null
|
|
|
|
};
|
|
|
|
|
|
|
|
// Validate the body again schema
|
2019-11-16 12:23:16 +10:00
|
|
|
const schemaValidate = validateJson('editProduct', productDoc);
|
|
|
|
if(!schemaValidate.result){
|
2020-03-17 18:38:46 +10:00
|
|
|
res.status(400).json(schemaValidate.errors);
|
2019-10-26 11:08:53 +10:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove productId from doc
|
|
|
|
delete productDoc.productId;
|
|
|
|
|
|
|
|
// if no featured image
|
|
|
|
if(!product.productImage){
|
|
|
|
if(images.length > 0){
|
2019-11-03 09:18:34 +10:00
|
|
|
productDoc.productImage = images[0].path;
|
2019-10-26 11:08:53 +10:00
|
|
|
}else{
|
2019-11-03 09:18:34 +10:00
|
|
|
productDoc.productImage = '/uploads/placeholder.png';
|
2019-10-26 11:08:53 +10:00
|
|
|
}
|
|
|
|
}else{
|
2019-11-03 09:18:34 +10:00
|
|
|
productDoc.productImage = product.productImage;
|
2019-10-26 11:08:53 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
try{
|
2019-10-29 18:26:30 +10:00
|
|
|
await db.products.updateOne({ _id: common.getId(req.body.productId) }, { $set: productDoc }, {});
|
2019-10-26 11:08:53 +10:00
|
|
|
// Update the index
|
|
|
|
indexProducts(req.app)
|
|
|
|
.then(() => {
|
2019-12-16 12:30:30 +10:00
|
|
|
res.status(200).json({ message: 'Successfully saved', product: productDoc });
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
2019-10-26 11:08:53 +10:00
|
|
|
}catch(ex){
|
2019-12-16 12:30:30 +10:00
|
|
|
res.status(400).json({ message: 'Failed to save. Please try again' });
|
2019-10-26 11:08:53 +10:00
|
|
|
}
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
|
2019-12-30 12:48:45 +10:00
|
|
|
// delete a product
|
2019-12-13 21:06:25 +10:00
|
|
|
router.post('/admin/product/delete', restrict, checkAccess, async (req, res) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
// remove the product
|
2019-12-13 21:06:25 +10:00
|
|
|
await db.products.deleteOne({ _id: common.getId(req.body.productId) }, {});
|
2019-10-26 11:08:53 +10:00
|
|
|
|
|
|
|
// delete any images and folder
|
2019-12-13 21:06:25 +10:00
|
|
|
rimraf('public/uploads/' + req.body.productId, (err) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
if(err){
|
|
|
|
console.info(err.stack);
|
2019-12-13 21:06:25 +10:00
|
|
|
res.status(400).json({ message: 'Failed to delete product' });
|
2018-02-05 23:20:44 +10:00
|
|
|
}
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
// re-index products
|
|
|
|
indexProducts(req.app)
|
|
|
|
.then(() => {
|
2019-12-13 21:06:25 +10:00
|
|
|
res.status(200).json({ message: 'Product successfully deleted' });
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// update the published state based on an ajax call from the frontend
|
2020-01-21 17:36:46 +10:00
|
|
|
router.post('/admin/product/publishedState', restrict, checkAccess, async (req, res) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
try{
|
2019-10-29 18:26:30 +10:00
|
|
|
await db.products.updateOne({ _id: common.getId(req.body.id) }, { $set: { productPublished: common.convertBool(req.body.state) } }, { multi: false });
|
2019-12-13 21:06:25 +10:00
|
|
|
res.status(200).json({ message: 'Published state updated' });
|
2019-10-26 11:08:53 +10:00
|
|
|
}catch(ex){
|
|
|
|
console.error(colors.red('Failed to update the published state: ' + ex));
|
2019-12-13 21:06:25 +10:00
|
|
|
res.status(400).json({ message: 'Published state not updated' });
|
2019-10-26 11:08:53 +10:00
|
|
|
}
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
// set as main product image
|
2019-10-26 11:08:53 +10:00
|
|
|
router.post('/admin/product/setasmainimage', restrict, checkAccess, async (req, res) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
|
|
|
|
2019-10-26 11:08:53 +10:00
|
|
|
try{
|
|
|
|
// update the productImage to the db
|
2019-10-29 18:26:30 +10:00
|
|
|
await db.products.updateOne({ _id: common.getId(req.body.product_id) }, { $set: { productImage: req.body.productImage } }, { multi: false });
|
2019-10-26 11:08:53 +10:00
|
|
|
res.status(200).json({ message: 'Main image successfully set' });
|
|
|
|
}catch(ex){
|
|
|
|
res.status(400).json({ message: 'Unable to set as main image. Please try again.' });
|
|
|
|
}
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
// deletes a product image
|
2019-10-26 11:08:53 +10:00
|
|
|
router.post('/admin/product/deleteimage', restrict, checkAccess, async (req, res) => {
|
2018-02-05 23:20:44 +10:00
|
|
|
const db = req.app.db;
|
|
|
|
|
|
|
|
// get the productImage from the db
|
2019-10-26 11:08:53 +10:00
|
|
|
const product = await db.products.findOne({ _id: common.getId(req.body.product_id) });
|
|
|
|
if(!product){
|
|
|
|
res.status(400).json({ message: 'Product not found' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if(req.body.productImage === product.productImage){
|
|
|
|
// set the productImage to null
|
2019-10-29 18:26:30 +10:00
|
|
|
await db.products.updateOne({ _id: common.getId(req.body.product_id) }, { $set: { productImage: null } }, { multi: false });
|
2019-10-26 11:08:53 +10:00
|
|
|
|
|
|
|
// remove the image from disk
|
|
|
|
fs.unlink(path.join('public', req.body.productImage), (err) => {
|
|
|
|
if(err){
|
|
|
|
res.status(400).json({ message: 'Image not removed, please try again.' });
|
|
|
|
}else{
|
|
|
|
res.status(200).json({ message: 'Image successfully deleted' });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}else{
|
|
|
|
// remove the image from disk
|
|
|
|
fs.unlink(path.join('public', req.body.productImage), (err) => {
|
|
|
|
if(err){
|
|
|
|
res.status(400).json({ message: 'Image not removed, please try again.' });
|
|
|
|
}else{
|
|
|
|
res.status(200).json({ message: 'Image successfully deleted' });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2018-02-05 23:20:44 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
module.exports = router;
|