Starting to add schema validation to API endpoints
parent
de5a01c671
commit
fc1580ddd6
42
app.js
42
app.js
|
@ -13,6 +13,7 @@ const colors = require('colors');
|
|||
const cron = require('node-cron');
|
||||
const common = require('./lib/common');
|
||||
const { runIndexing } = require('./lib/indexing');
|
||||
const { addSchemas } = require('./lib/schema');
|
||||
const { initDb } = require('./lib/db');
|
||||
let handlebars = require('express-handlebars');
|
||||
|
||||
|
@ -78,7 +79,7 @@ app.set('view engine', 'hbs');
|
|||
// helpers for the handlebar templating platform
|
||||
handlebars = handlebars.create({
|
||||
helpers: {
|
||||
perRowClass: function(numProducts){
|
||||
perRowClass: (numProducts) => {
|
||||
if(parseInt(numProducts) === 1){
|
||||
return'col-md-12 col-xl-12 col m12 xl12 product-item';
|
||||
}
|
||||
|
@ -94,7 +95,7 @@ handlebars = handlebars.create({
|
|||
|
||||
return'col-md-6 col-xl-6 col m6 xl6 product-item';
|
||||
},
|
||||
menuMatch: function(title, search){
|
||||
menuMatch: (title, search) => {
|
||||
if(!title || !search){
|
||||
return'';
|
||||
}
|
||||
|
@ -103,22 +104,22 @@ handlebars = handlebars.create({
|
|||
}
|
||||
return'';
|
||||
},
|
||||
getTheme: function(view){
|
||||
getTheme: (view) => {
|
||||
return`themes/${config.theme}/${view}`;
|
||||
},
|
||||
formatAmount: function(amt){
|
||||
formatAmount: (amt) => {
|
||||
if(amt){
|
||||
return numeral(amt).format('0.00');
|
||||
}
|
||||
return'0.00';
|
||||
},
|
||||
amountNoDecimal: function(amt){
|
||||
amountNoDecimal: (amt) => {
|
||||
if(amt){
|
||||
return handlebars.helpers.formatAmount(amt).replace('.', '');
|
||||
}
|
||||
return handlebars.helpers.formatAmount(amt);
|
||||
},
|
||||
getStatusColor: function (status){
|
||||
getStatusColor: (status) => {
|
||||
switch(status){
|
||||
case'Paid':
|
||||
return'success';
|
||||
|
@ -138,52 +139,58 @@ handlebars = handlebars.create({
|
|||
return'danger';
|
||||
}
|
||||
},
|
||||
checkProductOptions: function (opts){
|
||||
checkProductOptions: (opts) => {
|
||||
if(opts){
|
||||
return'true';
|
||||
}
|
||||
return'false';
|
||||
},
|
||||
currencySymbol: function(value){
|
||||
currencySymbol: (value) => {
|
||||
if(typeof value === 'undefined' || value === ''){
|
||||
return'$';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
objectLength: function(obj){
|
||||
objectLength: (obj) => {
|
||||
if(obj){
|
||||
return Object.keys(obj).length;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
checkedState: function (state){
|
||||
stringify: (obj) => {
|
||||
if(obj){
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
return'';
|
||||
},
|
||||
checkedState: (state) => {
|
||||
if(state === 'true' || state === true){
|
||||
return'checked';
|
||||
}
|
||||
return'';
|
||||
},
|
||||
selectState: function (state, value){
|
||||
selectState: (state, value) => {
|
||||
if(state === value){
|
||||
return'selected';
|
||||
}
|
||||
return'';
|
||||
},
|
||||
isNull: function (value, options){
|
||||
isNull: (value, options) => {
|
||||
if(typeof value === 'undefined' || value === ''){
|
||||
return options.fn(this);
|
||||
}
|
||||
return options.inverse(this);
|
||||
},
|
||||
toLower: function (value){
|
||||
toLower: (value) => {
|
||||
if(value){
|
||||
return value.toLowerCase();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
formatDate: function (date, format){
|
||||
formatDate: (date, format) => {
|
||||
return moment(date).format(format);
|
||||
},
|
||||
ifCond: function (v1, operator, v2, options){
|
||||
ifCond: (v1, operator, v2, options) => {
|
||||
switch(operator){
|
||||
case'==':
|
||||
return(v1 === v2) ? options.fn(this) : options.inverse(this);
|
||||
|
@ -207,7 +214,7 @@ handlebars = handlebars.create({
|
|||
return options.inverse(this);
|
||||
}
|
||||
},
|
||||
isAnAdmin: function (value, options){
|
||||
isAnAdmin: (value, options) => {
|
||||
if(value === 'true' || value === true){
|
||||
return options.fn(this);
|
||||
}
|
||||
|
@ -355,6 +362,9 @@ initDb(config.databaseConnectionString, async (err, db) => {
|
|||
config.trackStock = true;
|
||||
}
|
||||
|
||||
// Process schemas
|
||||
await addSchemas();
|
||||
|
||||
// We index when not in test env
|
||||
if(process.env.NODE_ENV !== 'test'){
|
||||
try{
|
||||
|
|
|
@ -40,6 +40,18 @@ const mongoSanitize = (param) => {
|
|||
return param;
|
||||
};
|
||||
|
||||
const safeParseInt = (param) => {
|
||||
if(param){
|
||||
try{
|
||||
return parseInt(param);
|
||||
}catch(ex){
|
||||
return param;
|
||||
}
|
||||
}else{
|
||||
return param;
|
||||
}
|
||||
};
|
||||
|
||||
const checkboxBool = (param) => {
|
||||
if(param && param === 'on'){
|
||||
return true;
|
||||
|
@ -47,6 +59,13 @@ const checkboxBool = (param) => {
|
|||
return false;
|
||||
};
|
||||
|
||||
const convertBool = (value) => {
|
||||
if(value === 'true' || value === true){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const showCartCloseBtn = (page) => {
|
||||
let showCartCloseButton = true;
|
||||
if(page === 'checkout' || page === 'pay'){
|
||||
|
@ -512,7 +531,9 @@ module.exports = {
|
|||
fileSizeLimit,
|
||||
cleanHtml,
|
||||
mongoSanitize,
|
||||
safeParseInt,
|
||||
checkboxBool,
|
||||
convertBool,
|
||||
showCartCloseBtn,
|
||||
addSitemapProducts,
|
||||
clearSessionValue,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
const Validator = require('jsonschema').Validator;
|
||||
const v = new Validator();
|
||||
const glob = require('glob-fs')();
|
||||
|
||||
const addSchemas = () => {
|
||||
const schemaFiles = glob.readdirSync('./lib/**/*.json');
|
||||
_.forEach(schemaFiles, (file) => {
|
||||
const fileData = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
v.addSchema(fileData, path.basename(schemaFiles[0], '.json'));
|
||||
});
|
||||
};
|
||||
|
||||
const validateJson = (schema, json) => {
|
||||
return v.validate(json, schema);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addSchemas,
|
||||
validateJson
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"id": "/editProduct",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"productPermalink": {
|
||||
"type": "string"
|
||||
},
|
||||
"productTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"productPrice": {
|
||||
"type": "number"
|
||||
},
|
||||
"productDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"productPublished": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"productTags": {
|
||||
"type": "string"
|
||||
},
|
||||
"productOptions": {
|
||||
"type": ["object", "string", "null"]
|
||||
},
|
||||
"productComment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"productStock": {
|
||||
"type": ["number", "null"]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"productPermalink",
|
||||
"productTitle",
|
||||
"productPrice",
|
||||
"productDescription",
|
||||
"productPublished"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"id": "/newProduct",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"productPermalink": {
|
||||
"type": "string"
|
||||
},
|
||||
"productTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"productPrice": {
|
||||
"type": "number"
|
||||
},
|
||||
"productDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"productPublished": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"productTags": {
|
||||
"type": "string"
|
||||
},
|
||||
"productOptions": {
|
||||
"type": ["object", "string", "null"]
|
||||
},
|
||||
"productComment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"productStock": {
|
||||
"type": ["number", "null"]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"productPermalink",
|
||||
"productTitle",
|
||||
"productPrice",
|
||||
"productDescription",
|
||||
"productPublished"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -24,8 +24,10 @@
|
|||
"express-handlebars": "^3.1.0",
|
||||
"express-session": "^1.16.1",
|
||||
"glob": "^7.1.4",
|
||||
"glob-fs": "^0.1.7",
|
||||
"helmet": "^3.18.0",
|
||||
"html-entities": "^1.2.0",
|
||||
"jsonschema": "^1.2.4",
|
||||
"lodash": "^4.17.11",
|
||||
"lunr": "^2.3.6",
|
||||
"mime-db": "^1.40.0",
|
||||
|
|
|
@ -103,7 +103,7 @@ $(document).ready(function (){
|
|||
$(this).addClass('table table-hover');
|
||||
});
|
||||
|
||||
$('#frmProductTags').tokenfield();
|
||||
$('#productTags').tokenfield();
|
||||
|
||||
$(document).on('click', '.dashboard_list', function(e){
|
||||
window.document.location = $(this).attr('href');
|
||||
|
@ -245,8 +245,8 @@ $(document).ready(function (){
|
|||
var optOptions = $('#product_optOptions').val();
|
||||
|
||||
var optJson = {};
|
||||
if($('#productOptJson').val() !== ''){
|
||||
optJson = JSON.parse($('#productOptJson').val());
|
||||
if($('#productOptions').val() !== ''){
|
||||
optJson = JSON.parse($('#productOptions').val());
|
||||
}
|
||||
|
||||
var html = '<li class="list-group-item">';
|
||||
|
@ -271,7 +271,7 @@ $(document).ready(function (){
|
|||
};
|
||||
|
||||
// write new json back to field
|
||||
$('#productOptJson').val(JSON.stringify(optJson));
|
||||
$('#productOptions').val(JSON.stringify(optJson));
|
||||
|
||||
// clear inputs
|
||||
$('#product_optName').val('');
|
||||
|
@ -593,14 +593,15 @@ $(document).ready(function (){
|
|||
|
||||
// Call to API to check if a permalink is available
|
||||
$(document).on('click', '#validate_permalink', function(e){
|
||||
if($('#frmProductPermalink').val() !== ''){
|
||||
if($('#productPermalink').val() !== ''){
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/admin/api/validate_permalink',
|
||||
data: { 'permalink': $('#frmProductPermalink').val(), 'docId': $('#frmProductId').val() }
|
||||
data: { 'permalink': $('#productPermalink').val(), 'docId': $('#frmProductId').val() }
|
||||
})
|
||||
.done(function(msg){
|
||||
showNotification(msg, 'success');
|
||||
console.log('msg', msg);
|
||||
showNotification(msg.message, 'success');
|
||||
})
|
||||
.fail(function(msg){
|
||||
showNotification(msg.responseJSON.message, 'danger');
|
||||
|
@ -654,8 +655,8 @@ $(document).ready(function (){
|
|||
|
||||
// create a permalink from the product title if no permalink has already been set
|
||||
$(document).on('click', '#frm_edit_product_save', function(e){
|
||||
if($('#frmProductPermalink').val() === '' && $('#frmProductTitle').val() !== ''){
|
||||
$('#frmProductPermalink').val(slugify($('#frmProductTitle').val()));
|
||||
if($('#productPermalink').val() === '' && $('#productTitle').val() !== ''){
|
||||
$('#productPermalink').val(slugify($('#productTitle').val()));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -147,10 +147,7 @@ router.get('/product/:id', (req, res) => {
|
|||
if(err || result == null || result.productPublished === 'false'){
|
||||
res.render('error', { title: 'Not found', message: 'Product not found', helpers: req.handlebars.helpers, config });
|
||||
}else{
|
||||
let productOptions = {};
|
||||
if(result.productOptions){
|
||||
productOptions = JSON.parse(result.productOptions);
|
||||
}
|
||||
let productOptions = result.productOptions;
|
||||
|
||||
// If JSON query param return json instead
|
||||
if(req.query.json === 'true'){
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
const express = require('express');
|
||||
const common = require('../lib/common');
|
||||
const { restrict, checkAccess } = require('../lib/auth');
|
||||
const { indexProducts } = require('../lib/indexing');
|
||||
const { validateJson } = require('../lib/schema');
|
||||
const colors = require('colors');
|
||||
const rimraf = require('rimraf');
|
||||
const fs = require('fs');
|
||||
|
@ -78,76 +80,132 @@ router.get('/admin/product/new', restrict, checkAccess, (req, res) => {
|
|||
router.post('/admin/product/insert', restrict, checkAccess, (req, res) => {
|
||||
const db = req.app.db;
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
let doc = {
|
||||
productPermalink: req.body.frmProductPermalink,
|
||||
productTitle: common.cleanHtml(req.body.frmProductTitle),
|
||||
productPrice: req.body.frmProductPrice,
|
||||
productDescription: common.cleanHtml(req.body.frmProductDescription),
|
||||
productPublished: req.body.frmProductPublished,
|
||||
productTags: req.body.frmProductTags,
|
||||
productOptions: common.cleanHtml(req.body.productOptJson),
|
||||
productComment: common.checkboxBool(req.body.frmProductComment),
|
||||
productPermalink: req.body.productPermalink,
|
||||
productTitle: common.cleanHtml(req.body.productTitle),
|
||||
productPrice: common.safeParseInt(req.body.productPrice),
|
||||
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),
|
||||
productAddedDate: new Date(),
|
||||
productStock: req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null
|
||||
productStock: common.safeParseInt(req.body.productStock) || null
|
||||
};
|
||||
|
||||
db.products.count({ 'productPermalink': req.body.frmProductPermalink }, (err, product) => {
|
||||
// Validate the body again schema
|
||||
const schemaResult = validateJson('newProduct', doc);
|
||||
if(!schemaResult.valid){
|
||||
// If API request, return json
|
||||
if(req.apiAuthenticated){
|
||||
res.status(400).json(schemaResult.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('schemaResult errors', schemaResult.errors);
|
||||
req.session.message = 'Form invalid. Please check values and try again.';
|
||||
req.session.messageType = 'danger';
|
||||
|
||||
// keep the current stuff
|
||||
req.session.productTitle = req.body.productTitle;
|
||||
req.session.productDescription = req.body.productDescription;
|
||||
req.session.productPrice = req.body.productPrice;
|
||||
req.session.productPermalink = req.body.productPermalink;
|
||||
req.session.productOptions = productOptions;
|
||||
req.session.productComment = common.checkboxBool(req.body.productComment);
|
||||
req.session.productTags = req.body.productTags;
|
||||
req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null;
|
||||
|
||||
// redirect to insert
|
||||
res.redirect('/admin/product/new');
|
||||
return;
|
||||
}
|
||||
|
||||
db.products.count({ 'productPermalink': req.body.productPermalink }, (err, product) => {
|
||||
if(err){
|
||||
console.info(err.stack);
|
||||
}
|
||||
if(product > 0 && req.body.frmProductPermalink !== ''){
|
||||
if(product > 0 && req.body.productPermalink !== ''){
|
||||
// permalink exits
|
||||
req.session.message = 'Permalink already exists. Pick a new one.';
|
||||
req.session.messageType = 'danger';
|
||||
|
||||
// keep the current stuff
|
||||
req.session.productTitle = req.body.frmProductTitle;
|
||||
req.session.productDescription = req.body.frmProductDescription;
|
||||
req.session.productPrice = req.body.frmProductPrice;
|
||||
req.session.productPermalink = req.body.frmProductPermalink;
|
||||
req.session.productPermalink = req.body.productOptJson;
|
||||
req.session.productComment = common.checkboxBool(req.body.frmProductComment);
|
||||
req.session.productTags = req.body.frmProductTags;
|
||||
req.session.productStock = req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null;
|
||||
req.session.productTitle = req.body.productTitle;
|
||||
req.session.productDescription = req.body.productDescription;
|
||||
req.session.productPrice = req.body.productPrice;
|
||||
req.session.productPermalink = req.body.productPermalink;
|
||||
req.session.productOptions = productOptions;
|
||||
req.session.productComment = common.checkboxBool(req.body.productComment);
|
||||
req.session.productTags = req.body.productTags;
|
||||
req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null;
|
||||
|
||||
// If API request, return json
|
||||
if(req.apiAuthenticated){
|
||||
res.status(400).json({ error: 'Permalink already exists. Pick a new one.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect to insert
|
||||
res.redirect('/admin/insert');
|
||||
}else{
|
||||
res.redirect('/admin/product/new');
|
||||
return;
|
||||
}
|
||||
db.products.insert(doc, (err, newDoc) => {
|
||||
if(err){
|
||||
console.log(colors.red('Error inserting document: ' + err));
|
||||
|
||||
// keep the current stuff
|
||||
req.session.productTitle = req.body.frmProductTitle;
|
||||
req.session.productDescription = req.body.frmProductDescription;
|
||||
req.session.productPrice = req.body.frmProductPrice;
|
||||
req.session.productPermalink = req.body.frmProductPermalink;
|
||||
req.session.productPermalink = req.body.productOptJson;
|
||||
req.session.productComment = common.checkboxBool(req.body.frmProductComment);
|
||||
req.session.productTags = req.body.frmProductTags;
|
||||
req.session.productStock = req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null;
|
||||
req.session.productTitle = req.body.productTitle;
|
||||
req.session.productDescription = req.body.productDescription;
|
||||
req.session.productPrice = req.body.productPrice;
|
||||
req.session.productPermalink = req.body.productPermalink;
|
||||
req.session.productOptions = productOptions;
|
||||
req.session.productComment = common.checkboxBool(req.body.productComment);
|
||||
req.session.productTags = req.body.productTags;
|
||||
req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null;
|
||||
|
||||
req.session.message = 'Error: Inserting product';
|
||||
req.session.messageType = 'danger';
|
||||
|
||||
// If API request, return json
|
||||
if(req.apiAuthenticated){
|
||||
res.status(400).json({ error: `Error inserting document: ${err}` });
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect to insert
|
||||
res.redirect('/admin/product/new');
|
||||
}else{
|
||||
return;
|
||||
}
|
||||
// get the new ID
|
||||
let newId = newDoc.insertedIds[0];
|
||||
|
||||
// add to lunr index
|
||||
common.indexProducts(req.app)
|
||||
indexProducts(req.app)
|
||||
.then(() => {
|
||||
req.session.message = 'New product successfully created';
|
||||
req.session.messageType = 'success';
|
||||
|
||||
// If API request, return json
|
||||
if(req.apiAuthenticated){
|
||||
res.status(200).json({ error: 'New product successfully created' });
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect to new doc
|
||||
res.redirect('/admin/product/edit/' + newId);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -162,7 +220,7 @@ router.get('/admin/product/edit/:id', restrict, checkAccess, (req, res) => {
|
|||
}
|
||||
let options = {};
|
||||
if(result.productOptions){
|
||||
options = JSON.parse(result.productOptions);
|
||||
options = result.productOptions;
|
||||
}
|
||||
|
||||
res.render('product_edit', {
|
||||
|
@ -194,7 +252,7 @@ router.post('/admin/product/update', restrict, checkAccess, (req, res) => {
|
|||
res.redirect('/admin/product/edit/' + req.body.frmProductId);
|
||||
return;
|
||||
}
|
||||
db.products.count({ 'productPermalink': req.body.frmProductPermalink, _id: { $ne: common.getId(product._id) } }, (err, count) => {
|
||||
db.products.count({ 'productPermalink': req.body.productPermalink, _id: { $ne: common.getId(product._id) } }, (err, count) => {
|
||||
if(err){
|
||||
console.info(err.stack);
|
||||
req.session.message = 'Failed updating product.';
|
||||
|
@ -203,37 +261,75 @@ router.post('/admin/product/update', restrict, checkAccess, (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if(count > 0 && req.body.frmProductPermalink !== ''){
|
||||
if(count > 0 && req.body.productPermalink !== ''){
|
||||
// permalink exits
|
||||
req.session.message = 'Permalink already exists. Pick a new one.';
|
||||
req.session.messageType = 'danger';
|
||||
|
||||
// keep the current stuff
|
||||
req.session.productTitle = req.body.frmProductTitle;
|
||||
req.session.productDescription = req.body.frmProductDescription;
|
||||
req.session.productPrice = req.body.frmProductPrice;
|
||||
req.session.productPermalink = req.body.frmProductPermalink;
|
||||
req.session.productTags = req.body.frmProductTags;
|
||||
req.session.productOptions = req.body.productOptJson;
|
||||
req.session.productComment = common.checkboxBool(req.body.frmProductComment);
|
||||
req.session.productStock = req.body.frmProductStock ? req.body.frmProductStock : null;
|
||||
req.session.productTitle = req.body.productTitle;
|
||||
req.session.productDescription = req.body.productDescription;
|
||||
req.session.productPrice = req.body.productPrice;
|
||||
req.session.productPermalink = req.body.productPermalink;
|
||||
req.session.productTags = req.body.productTags;
|
||||
req.session.productOptions = req.body.productOptions;
|
||||
req.session.productComment = common.checkboxBool(req.body.productComment);
|
||||
req.session.productStock = req.body.productStock ? req.body.productStock : null;
|
||||
|
||||
// redirect to insert
|
||||
res.redirect('/admin/product/edit/' + req.body.frmProductId);
|
||||
}else{
|
||||
common.getImages(req.body.frmProductId, req, res, (images) => {
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
let productDoc = {
|
||||
productTitle: common.cleanHtml(req.body.frmProductTitle),
|
||||
productDescription: common.cleanHtml(req.body.frmProductDescription),
|
||||
productPublished: req.body.frmProductPublished,
|
||||
productPrice: req.body.frmProductPrice,
|
||||
productPermalink: req.body.frmProductPermalink,
|
||||
productTags: common.cleanHtml(req.body.frmProductTags),
|
||||
productOptions: common.cleanHtml(req.body.productOptJson),
|
||||
productComment: common.checkboxBool(req.body.frmProductComment),
|
||||
productStock: req.body.frmProductStock ? parseInt(req.body.frmProductStock) : null
|
||||
productPermalink: req.body.productPermalink,
|
||||
productTitle: common.cleanHtml(req.body.productTitle),
|
||||
productPrice: common.safeParseInt(req.body.productPrice),
|
||||
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
|
||||
const schemaResult = validateJson('editProduct', productDoc);
|
||||
if(!schemaResult.valid){
|
||||
// If API request, return json
|
||||
if(req.apiAuthenticated){
|
||||
res.status(400).json(schemaResult.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('schemaResult errors', schemaResult.errors);
|
||||
req.session.message = 'Form invalid. Please check values and try again.';
|
||||
req.session.messageType = 'danger';
|
||||
|
||||
// keep the current stuff
|
||||
req.session.productTitle = req.body.productTitle;
|
||||
req.session.productDescription = req.body.productDescription;
|
||||
req.session.productPrice = req.body.productPrice;
|
||||
req.session.productPermalink = req.body.productPermalink;
|
||||
req.session.productOptions = productOptions;
|
||||
req.session.productComment = common.checkboxBool(req.body.productComment);
|
||||
req.session.productTags = req.body.productTags;
|
||||
req.session.productStock = req.body.productStock ? parseInt(req.body.productStock) : null;
|
||||
|
||||
// redirect to insert
|
||||
res.redirect('/admin/product/edit/' + req.body.frmProductId);
|
||||
return;
|
||||
}
|
||||
|
||||
// if no featured image
|
||||
if(!product.productImage){
|
||||
if(images.length > 0){
|
||||
|
@ -253,7 +349,7 @@ router.post('/admin/product/update', restrict, checkAccess, (req, res) => {
|
|||
res.redirect('/admin/product/edit/' + req.body.frmProductId);
|
||||
}else{
|
||||
// Update the index
|
||||
common.indexProducts(req.app)
|
||||
indexProducts(req.app)
|
||||
.then(() => {
|
||||
req.session.message = 'Successfully saved';
|
||||
req.session.messageType = 'success';
|
||||
|
@ -283,7 +379,7 @@ router.get('/admin/product/delete/:id', restrict, checkAccess, (req, res) => {
|
|||
}
|
||||
|
||||
// remove the index
|
||||
common.indexProducts(req.app)
|
||||
indexProducts(req.app)
|
||||
.then(() => {
|
||||
// redirect home
|
||||
req.session.message = 'Product successfully deleted';
|
||||
|
|
|
@ -10,24 +10,24 @@
|
|||
<h2>Edit product</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductTitle" class="col-sm-2 control-label">Product title *</label>
|
||||
<label for="productTitle" class="col-sm-2 control-label">Product title *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="frmProductTitle" class="form-control" minlength="5" maxlength="200" value="{{result.productTitle}}" required/>
|
||||
<input type="text" name="productTitle" class="form-control" minlength="5" maxlength="200" value="{{result.productTitle}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductPrice" class="col-sm-2 control-label">Product price *</label>
|
||||
<label for="productPrice" class="col-sm-2 control-label">Product price *</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">{{currencySymbol config.currencySymbol}}</span>
|
||||
<input type="number" name="frmProductPrice" class="form-control" value="{{result.productPrice}}" step="any" required/>
|
||||
<input type="number" name="productPrice" class="form-control" value="{{result.productPrice}}" step="any" required/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductPublished" class="col-sm-2 control-label">Status</label>
|
||||
<label for="productPublished" class="col-sm-2 control-label">Status</label>
|
||||
<div class="col-sm-6">
|
||||
<select class="form-control" id="frmProductPublished" name="frmProductPublished">
|
||||
<select class="form-control" id="productPublished" name="productPublished">
|
||||
<option value="true" {{selectState result.productPublished "true"}}>Published</option>
|
||||
<option value="false" {{selectState result.productPublished "false"}}>Draft</option>
|
||||
</select>
|
||||
|
@ -35,23 +35,23 @@
|
|||
</div>
|
||||
{{#if config.trackStock}}
|
||||
<div class="form-group">
|
||||
<label for="frmProductStock" class="col-sm-2 control-label">Stock level</label>
|
||||
<label for="productStock" class="col-sm-2 control-label">Stock level</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="number" name="frmProductStock" class="form-control" value="{{result.productStock}}" step="any" />
|
||||
<input type="number" name="productStock" class="form-control" value="{{result.productStock}}" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="form-group">
|
||||
<label for="editor" class="col-sm-2 control-label">Product description *</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea id="editor" minlength="5" rows="10" id="frmProductDescription" name="frmProductDescription" class="form-control" required>{{result.productDescription}}</textarea>
|
||||
<textarea id="editor" minlength="5" rows="10" id="productDescription" name="productDescription" class="form-control" required>{{result.productDescription}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Permalink</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="frmProductPermalink" id="frmProductPermalink" placeholder="Permalink for the article" value={{result.productPermalink}}>
|
||||
<input type="text" class="form-control" name="productPermalink" id="productPermalink" placeholder="Permalink for the article" value={{result.productPermalink}}>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-success" id="validate_permalink" type="button">Validate</button>
|
||||
</span>
|
||||
|
@ -60,7 +60,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="hidden" id="productOptJson" name="productOptJson" value="{{result.productOptions}}" />
|
||||
<input type="hidden" id="productOptions" name="productOptions" value="{{stringify result.productOptions}}" />
|
||||
<label for="editor" class="col-sm-2 control-label">Product options</label>
|
||||
<div class="col-lg-10">
|
||||
<ul class="list-group" id="product_opt_wrapper">
|
||||
|
@ -108,21 +108,21 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductComment" class="col-sm-2 control-label">Allow comment</label>
|
||||
<label for="productComment" class="col-sm-2 control-label">Allow comment</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="frmProductComment" type="checkbox" {{checkedState result.productComment}} id="frmProductComment"
|
||||
name="frmProductComment">
|
||||
<input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment"
|
||||
name="productComment">
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">Allow free form comments when adding products to cart</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductTags" class="col-sm-2 control-label">Product tag words</label>
|
||||
<label for="productTags" class="col-sm-2 control-label">Product tag words</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="frmProductTags" name="frmProductTags" value="{{result.productTags}}">
|
||||
<input type="text" class="form-control" id="productTags" name="productTags" value="{{result.productTags}}">
|
||||
<p class="help-block">Tag words used to indexed products, making them easier to find and filter.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,24 +9,24 @@
|
|||
<h2>New product</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductTitle" class="col-sm-2 control-label">Product title *</label>
|
||||
<label for="productTitle" class="col-sm-2 control-label">Product title *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" id="frmProductTitle" name="frmProductTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/>
|
||||
<input type="text" id="productTitle" name="productTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductPrice" class="col-sm-2 control-label">Product price *</label>
|
||||
<label for="productPrice" class="col-sm-2 control-label">Product price *</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">$</span>
|
||||
<input type="number" name="frmProductPrice" class="form-control" step="any" value="{{productPrice}}" required/>
|
||||
<input type="number" name="productPrice" class="form-control" step="any" value="{{productPrice}}" required/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductPublished" class="col-sm-2 control-label">Status</label>
|
||||
<label for="productPublished" class="col-sm-2 control-label">Status</label>
|
||||
<div class="col-sm-6">
|
||||
<select class="form-control" id="frmProductPublished" name="frmProductPublished">
|
||||
<select class="form-control" id="productPublished" name="productPublished">
|
||||
<option value="true" selected>Published</option>
|
||||
<option value="false">Draft</option>
|
||||
</select>
|
||||
|
@ -34,23 +34,23 @@
|
|||
</div>
|
||||
{{#if config.trackStock}}
|
||||
<div class="form-group">
|
||||
<label for="frmProductStock" class="col-sm-2 control-label">Stock level</label>
|
||||
<label for="productStock" class="col-sm-2 control-label">Stock level</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="number" name="frmProductStock" class="form-control" value="{{productStock}}" step="any" />
|
||||
<input type="number" name="productStock" class="form-control" value="{{productStock}}" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="form-group" id="editor-wrapper">
|
||||
<label for="editor" class="col-sm-2 control-label">Product description *</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea id="editor" minlength="5" rows="10" name="frmProductDescription" class="form-control" required>{{productDescription}}</textarea>
|
||||
<textarea id="editor" minlength="5" rows="10" name="productDescription" class="form-control" required>{{productDescription}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Permalink</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="frmProductPermalink" id="frmProductPermalink" placeholder="Permalink for the article" value={{productPermalink}}>
|
||||
<input type="text" class="form-control" name="productPermalink" id="productPermalink" placeholder="Permalink for the article" value={{productPermalink}}>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-success" id="validate_permalink" type="button">Validate</button>
|
||||
</span>
|
||||
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="hidden" id="productOptJson" name="productOptJson" value="{{result.productOptions}}" />
|
||||
<input type="hidden" id="productOptions" name="productOptions" value="{{result.productOptions}}" />
|
||||
<label for="editor" class="col-sm-2 control-label">Product options</label>
|
||||
<div class="col-lg-10">
|
||||
<ul class="list-group" id="product_opt_wrapper">
|
||||
|
@ -107,21 +107,21 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductComment" class="col-sm-2 control-label">Allow comment</label>
|
||||
<label for="productComment" class="col-sm-2 control-label">Allow comment</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="frmProductComment" type="checkbox" {{checkedState result.productComment}} id="frmProductComment"
|
||||
name="frmProductComment">
|
||||
<input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment"
|
||||
name="productComment">
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">Allow free form comments when adding products to cart</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frmProductTags" class="col-sm-2 control-label">Product tag words</label>
|
||||
<label for="productTags" class="col-sm-2 control-label">Product tag words</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="frmProductTags" name="frmProductTags">
|
||||
<input type="text" class="form-control" id="productTags" name="productTags">
|
||||
<p class="help-block">Tag words used to indexed products, making them easier to find and filter.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,17 +12,17 @@
|
|||
{{#ifCond this.optType '==' "select"}}
|
||||
<strong>{{this.optName}}</strong>
|
||||
<select name="opt-{{this.optName}}" class="form-control product-opt">
|
||||
{{#each this.optOptions}}
|
||||
{{#each ../this.optOptions}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{/ifCond}}
|
||||
{{#ifCond this.optType '==' "radio"}}
|
||||
{{#each this.optOptions}}
|
||||
{{#each ../this.optOptions}}
|
||||
<strong>{{this.optName}}</strong>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" class="product-opt" name="opt-{{../this.optName}}" value="{{this}}">
|
||||
<input type="radio" class="product-opt" name="opt-{{../../this.optName}}" value="{{this}}">
|
||||
{{this}}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@
|
|||
{{#ifCond this.optType '==' "checkbox"}}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{this.optName}}"><strong>{{this.optName}}</strong>
|
||||
<input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{../this.optName}}"><strong>{{../this.optName}}</strong>
|
||||
</label>
|
||||
</div>
|
||||
{{/ifCond}}
|
||||
|
|
|
@ -12,17 +12,17 @@
|
|||
{{#ifCond this.optType '==' "select"}}
|
||||
<strong>{{this.optName}}</strong>
|
||||
<select name="opt-{{this.optName}}" class="form-control product-opt">
|
||||
{{#each this.optOptions}}
|
||||
{{#each ../this.optOptions}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{/ifCond}}
|
||||
{{#ifCond this.optType '==' "radio"}}
|
||||
{{#each this.optOptions}}
|
||||
{{#each ../this.optOptions}}
|
||||
<strong>{{this.optName}}</strong>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" class="product-opt" name="opt-{{../this.optName}}" value="{{this}}">
|
||||
<input type="radio" class="product-opt" name="opt-{{../../this.optName}}" value="{{this}}">
|
||||
{{this}}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@
|
|||
{{#ifCond this.optType '==' "checkbox"}}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{this.optName}}"><strong>{{this.optName}}</strong>
|
||||
<input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{../this.optName}}"><strong>{{../this.optName}}</strong>
|
||||
</label>
|
||||
</div>
|
||||
{{/ifCond}}
|
||||
|
|
|
@ -12,17 +12,17 @@
|
|||
{{#ifCond this.optType '==' "select"}}
|
||||
<strong>{{this.optName}}</strong>
|
||||
<select name="opt-{{this.optName}}" class="form-control product-opt">
|
||||
{{#each this.optOptions}}
|
||||
{{#each ../this.optOptions}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{/ifCond}}
|
||||
{{#ifCond this.optType '==' "radio"}}
|
||||
{{#each this.optOptions}}
|
||||
{{#each ../this.optOptions}}
|
||||
<strong>{{this.optName}}</strong>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" class="product-opt" name="opt-{{../this.optName}}" value="{{this}}">
|
||||
<input type="radio" class="product-opt" name="opt-{{../../this.optName}}" value="{{this}}">
|
||||
{{this}}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@
|
|||
{{#ifCond this.optType '==' "checkbox"}}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{this.optName}}"><strong>{{this.optName}}</strong>
|
||||
<input type="checkbox" class="product-opt" name="opt-{{../this.optName}}" value="{{../this.optName}}"><strong>{{../this.optName}}</strong>
|
||||
</label>
|
||||
</div>
|
||||
{{/ifCond}}
|
||||
|
|
Loading…
Reference in New Issue