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