diff --git a/controller/admin.controller.js b/controller/admin.controller.js new file mode 100644 index 00000000..16e1c4e2 --- /dev/null +++ b/controller/admin.controller.js @@ -0,0 +1,911 @@ +const express = require('express'); +const { restrict, checkAccess } = require('../lib/auth'); +const escape = require('html-entities').AllHtmlEntities; +const colors = require('colors'); +const moment = require('moment'); +const fs = require('fs'); +const path = require('path'); +const multer = require('multer'); +const mime = require('mime-type/with-db'); +const csrf = require('csurf'); +const util = require('util'); +const stream = require('stream'); +const { validateJson } = require('../lib/schema'); + +const{clearSessionValue, + mongoSanitize, + getThemes, + getId, + allowedMimeType, + fileSizeLimit, + checkDirectorySync, + sendEmail }= require('../lib/common'); +const bcrypt = require('bcryptjs'); +const { + getConfig, + updateConfig +} = require('../lib/config'); +const { + sortMenu, + getMenu, + newMenu, + updateMenu, + deleteMenu, + orderMenu +} = require('../lib/menu'); + +// Admin section +const adminDashboard=( (req, res, next)=>{ + res.redirect('/admin/dashboard'); +}) +// logout 1 +const logout=( (req, res)=>{ + req.session.user = null; + req.session.message = null; + req.session.messageType = null; + res.redirect('/'); +}) + +// login form +const login=(async (req, res) => { + const db = req.app.db; + + const userCount = await db.users.countDocuments({}); + // we check for a user. If one exists, redirect to login form otherwise setup + if(userCount && userCount > 0){ + // set needsSetup to false as a user exists + req.session.needsSetup = false; + res.render('login', { + title: 'Login', + referringUrl: req.header('Referer'), + config: req.app.config, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + showFooter: 'showFooter' + }); + }else{ + // if there are no users set the "needsSetup" session + req.session.needsSetup = true; + res.redirect('/admin/setup'); + } +}); + +// login the user and check the password +const loginValidate=( async (req, res) => { + const db = req.app.db; + + const user = await db.users.findOne({ userEmail: mongoSanitize(req.body.email) }); + if(!user || user === null){ + res.status(400).json({ message: 'A user with that email does not exist.' }); + return; + } + + // we have a user under that email so we compare the password + bcrypt.compare(req.body.password, user.userPassword) + .then((result) => { + if(result){ + req.session.user = req.body.email; + req.session.usersName = user.usersName; + req.session.userId = user._id.toString(); + req.session.isAdmin = user.isAdmin; + res.status(200).json({ message: 'Login successful' }); + return; + } + // password is not correct + res.status(400).json({ message: 'Access denied. Check password and try again.' }); + }); +}); + + +// setup form is shown when there are no users setup in the DB +const adminSetup=(async (req, res) => { + const db = req.app.db; + + const userCount = await db.users.countDocuments({}); + // dont allow the user to "re-setup" if a user exists. + // set needsSetup to false as a user exists + req.session.needsSetup = false; + if(userCount === 0){ + req.session.needsSetup = true; + res.render('setup', { + title: 'Setup', + config: req.app.config, + helpers: req.handlebars.helpers, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + showFooter: 'showFooter' + }); + return; + } + res.redirect('/admin/login'); +}); + + + +// insert a user 5 +const setupUser=(async (req, res) => { + const db = req.app.db; + + const doc = { + usersName: req.body.usersName, + userEmail: req.body.userEmail, + userPassword: bcrypt.hashSync(req.body.userPassword, 10), + isAdmin: true, + isOwner: true + }; + + // check for users + const userCount = await db.users.countDocuments({}); + if(userCount === 0){ + // email is ok to be used. + try{ + await db.users.insertOne(doc); + res.status(200).json({ message: 'User account inserted' }); + return; + }catch(ex){ + console.error(colors.red(`Failed to insert user: ${ex}`)); + res.status(200).json({ message: 'Setup failed' }); + return; + } + } + res.status(200).json({ message: 'Already setup.' }); +}); + + +// dashboard +const dashboard=(async (req, res) => { + const db = req.app.db; + + // Collate data for dashboard + const dashboardData = { + productsCount: await db.products.countDocuments({ + productPublished: true + }), + ordersCount: await db.orders.countDocuments({}), + ordersAmount: await db.orders.aggregate([{ $match: {} }, + { $group: { _id: null, sum: { $sum: '$orderTotal' } } + }]).toArray(), + productsSold: await db.orders.aggregate([{ $match: {} }, + { $group: { _id: null, sum: { $sum: '$orderProductCount' } } + }]).toArray(), + topProducts: await db.orders.aggregate([ + { $project: { _id: 0 } }, + { $project: { o: { $objectToArray: '$orderProducts' } } }, + { $unwind: '$o' }, + { $group: { + _id: '$o.v.title', + productImage: { $last: '$o.v.productImage' }, + count: { $sum: '$o.v.quantity' } + } }, + { $sort: { count: -1 } }, + { $limit: 5 } + ]).toArray() + }; + + // Fix aggregate data + if(dashboardData.ordersAmount.length > 0){ + dashboardData.ordersAmount = dashboardData.ordersAmount[0].sum; + } + if(dashboardData.productsSold.length > 0){ + dashboardData.productsSold = dashboardData.productsSold[0].sum; + }else{ + dashboardData.productsSold = 0; + } + + res.render('dashboard', { + title: 'Cart dashboard', + session: req.session, + admin: true, + dashboardData, + themes: getThemes(), + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + csrfToken: req.csrfToken() + }); +}); + +// settings +const getSettings=((req, res) => { + res.render('settings', { + title: 'Cart settings', + session: req.session, + admin: true, + themes: getThemes(), + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + footerHtml: typeof req.app.config.footerHtml !== 'undefined' ? escape.decode(req.app.config.footerHtml) : null, + googleAnalytics: typeof req.app.config.googleAnalytics !== 'undefined' ? escape.decode(req.app.config.googleAnalytics) : null, + csrfToken: req.csrfToken() + }); +}); + +// create API key +const genAPI=(async (req, res) => { + const db = req.app.db; + const result = await db.users.findOneAndUpdate({ + _id: ObjectId(req.session.userId), + isAdmin: true + }, { + $set: { + apiKey: new ObjectId() + } + }, { + returnOriginal: false + }); + + if(result.value && result.value.apiKey){ + res.status(200).json({ message: 'API Key generated', apiKey: result.value.apiKey }); + return; + } + res.status(400).json({ message: 'Failed to generate API Key' }); +}); + +// settings update +function settingsUpdate(req, res) { + const result = updateConfig(req.body); + if(result === true){ + req.app.config = getConfig(); + res.status(200).json({ message: 'Settings successfully updated' }); + return; + } + res.status(400).json({ message: 'Permission denied' }); +}; + +// settings menu +const settingsMenu=(async (req, res) => { + const db = req.app.db; + res.render('settings-menu', { + title: 'Cart menu', + session: req.session, + admin: true, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + menu: sortMenu(await getMenu(db)), + csrfToken: req.csrfToken() + }); +}); + +// page list +const getPages=(async (req, res) => { + const db = req.app.db; + const pages = await db.pages.find({}).toArray(); + + res.render('settings-pages', { + title: 'Static pages', + pages: pages, + session: req.session, + admin: true, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + menu: sortMenu(await getMenu(db)), + csrfToken: req.csrfToken() + }); +}); + +// pages new +const newPages=(async (req, res) => { + const db = req.app.db; + + res.render('settings-page', { + title: 'Static pages', + session: req.session, + admin: true, + button_text: 'Create', + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + menu: sortMenu(await getMenu(db)), + csrfToken: req.csrfToken() + }); +}); + + +// pages editor +const editPage=(async (req, res) => { + const db = req.app.db; + const page = await db.pages.findOne({ _id: getId(req.params.page) }); + const menu = sortMenu(await getMenu(db)); + if(!page){ + res.status(404).render('error', { + title: '404 Error - Page not found', + config: req.app.config, + message: '404 Error - Page not found', + helpers: req.handlebars.helpers, + showFooter: 'showFooter', + menu + }); + return; + } + + res.render('settings-page', { + title: 'Static pages', + page: page, + button_text: 'Update', + session: req.session, + admin: true, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + menu, + csrfToken: req.csrfToken() + }); +}); + + + + + + + + + + + +// insert/update page +const insertPage=(async (req, res) => { + const db = req.app.db; + + const doc = { + pageName: req.body.pageName, + pageSlug: req.body.pageSlug, + pageEnabled: req.body.pageEnabled, + pageContent: req.body.pageContent + }; + + if(req.body.pageId){ + // existing page + const page = await db.pages.findOne({ _id: getId(req.body.pageId) }); + if(!page){ + res.status(400).json({ message: 'Page not found' }); + return; + } + + try{ + const updatedPage = await db.pages.findOneAndUpdate({ _id: getId(req.body.pageId) }, { $set: doc }, { returnOriginal: false }); + res.status(200).json({ message: 'Page updated successfully', pageId: req.body.pageId, page: updatedPage.value }); + }catch(ex){ + res.status(400).json({ message: 'Error updating page. Please try again.' }); + } + }else{ + // insert page + try{ + const newDoc = await db.pages.insertOne(doc); + res.status(200).json({ message: 'New page successfully created', pageId: newDoc.insertedId }); + return; + }catch(ex){ + res.status(400).json({ message: 'Error creating page. Please try again.' }); + } + } +}); + +// delete a page +const deletePage=(async (req, res) => { + const db = req.app.db; + + const page = await db.pages.findOne({ _id: getId(req.body.pageId) }); + if(!page){ + res.status(400).json({ message: 'Page not found' }); + return; + } + + try{ + await db.pages.deleteOne({ _id: getId(req.body.pageId) }, {}); + res.status(200).json({ message: 'Page successfully deleted' }); + return; + }catch(ex){ + res.status(400).json({ message: 'Error deleting page. Please try again.' }); + } +}); + +// new menu item +const menuItem=( (req, res) => { + const result = newMenu(req); + if(result === false){ + res.status(400).json({ message: 'Failed creating menu.' }); + return; + } + res.status(200).json({ message: 'Menu created successfully.' }); +}); + +// update existing menu item +const updatemenu=( (req, res) => { + const result = updateMenu(req); + if(result === false){ + res.status(400).json({ message: 'Failed updating menu.' }); + return; + } + res.status(200).json({ message: 'Menu updated successfully.' }); +}); + +// delete menu item +const deletemenu=((req, res) => { + const result = deleteMenu(req, req.body.menuId); + if(result === false){ + res.status(400).json({ message: 'Failed deleting menu.' }); + return; + } + res.status(200).json({ message: 'Menu deleted successfully.' }); +}); + +// We call this via a Ajax call to save the order from the sortable list +const saveOrder=( (req, res) => { + const result = orderMenu(req, res); + if(result === false){ + res.status(400).json({ message: 'Failed saving menu order' }); + return; + } + res.status(200).json({}); +}); + + + + + + + + + + + + +const validatePerma=(async (req, res) => { + // if doc id is provided it checks for permalink in any products other that one provided, + // else it just checks for any products with that permalink + const db = req.app.db; + + let query = {}; + if(typeof req.body.docId === 'undefined' || req.body.docId === ''){ + query = { productPermalink: req.body.permalink }; + }else{ + query = { productPermalink: req.body.permalink, _id: { $ne: getId(req.body.docId) } }; + } + + const products = await db.products.countDocuments(query); + if(products && products > 0){ + res.status(400).json({ message: 'Permalink already exists' }); + return; + } + res.status(200).json({ message: 'Permalink validated successfully' }); +}); + +// Discount codes +const discount=(async (req, res) => { + const db = req.app.db; + + const discounts = await db.discounts.find({}).toArray(); + + res.render('settings-discounts', { + title: 'Discount code', + config: req.app.config, + session: req.session, + discounts, + admin: true, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + csrfToken: req.csrfToken() + }); +}); + +// Edit a discount code +const editDiscount=( async (req, res) => { + const db = req.app.db; + + const discount = await db.discounts.findOne({ _id: getId(req.params.id) }); + + res.render('settings-discount-edit', { + title: 'Discount code edit', + session: req.session, + admin: true, + discount, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + csrfToken: req.csrfToken() + }); +}); + +// Update discount code +const updateDiscount=(async (req, res) => { + const db = req.app.db; + + // Doc to insert + const discountDoc = { + discountId: req.body.discountId, + code: req.body.code, + type: req.body.type, + value: parseInt(req.body.value), + start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(), + end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate() + }; + + // Validate the body again schema + const schemaValidate = validateJson('editDiscount', discountDoc); + if(!schemaValidate.result){ + res.status(400).json(schemaValidate.errors); + return; + } + + // Check start is after today + if(moment(discountDoc.start).isBefore(moment())){ + res.status(400).json({ message: 'Discount start date needs to be after today' }); + return; + } + + // Check end is after the start + if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){ + res.status(400).json({ message: 'Discount end date needs to be after start date' }); + return; + } + + // Check if code exists + const checkCode = await db.discounts.countDocuments({ + code: discountDoc.code, + _id: { $ne: getId(discountDoc.discountId) } + }); + if(checkCode){ + res.status(400).json({ message: 'Discount code already exists' }); + return; + } + + // Remove discountID + delete discountDoc.discountId; + + try{ + await db.discounts.updateOne({ _id: getId(req.body.discountId) }, { $set: discountDoc }, {}); + res.status(200).json({ message: 'Successfully saved', discount: discountDoc }); + }catch(ex){ + res.status(400).json({ message: 'Failed to save. Please try again' }); + } +}); + +// Create a discount code +const getDiscount=( async (req, res) => { + res.render('settings-discount-new', { + title: 'Discount code create', + session: req.session, + admin: true, + message: clearSessionValue(req.session, 'message'), + messageType: clearSessionValue(req.session, 'messageType'), + helpers: req.handlebars.helpers, + config: req.app.config, + csrfToken: req.csrfToken() + }); +}); + +// Create a discount code +const createDiscount=( async (req, res) => { + const db = req.app.db; + + // Doc to insert + const discountDoc = { + code: req.body.code, + type: req.body.type, + value: parseInt(req.body.value), + start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(), + end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate() + }; + + // Validate the body again schema + const schemaValidate = validateJson('newDiscount', discountDoc); + if(!schemaValidate.result){ + res.status(400).json(schemaValidate.errors); + return; + } + + // Check if code exists + const checkCode = await db.discounts.countDocuments({ + code: discountDoc.code + }); + if(checkCode){ + res.status(400).json({ message: 'Discount code already exists' }); + return; + } + + // Check start is after today + if(moment(discountDoc.start).isBefore(moment())){ + res.status(400).json({ message: 'Discount start date needs to be after today' }); + return; + } + + // Check end is after the start + if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){ + res.status(400).json({ message: 'Discount end date needs to be after start date' }); + return; + } + + // Insert discount code + const discount = await db.discounts.insertOne(discountDoc); + res.status(200).json({ message: 'Discount code created successfully', discountId: discount.insertedId }); +}); + +// Delete discount code +const deleteDiscount=(async (req, res) => { + const db = req.app.db; + + try{ + await db.discounts.deleteOne({ _id: getId(req.body.discountId) }, {}); + res.status(200).json({ message: 'Discount code successfully deleted' }); + return; + }catch(ex){ + res.status(400).json({ message: 'Error deleting discount code. Please try again.' }); + } +}); + +// Add image by URL +const addImage=(async (req, res) => { + const db = req.app.db; + + // get the product form the DB + const product = await db.products.findOne({ _id: getId(req.body.productId) }); + if(!product){ + // Return error + res.status(400).json({ message: 'Image error. Please try again.' }); + return; + } + + // Check image URL already in list + if(product.productImages){ + if(product.productImages.includes(req.body.imageUrl)){ + res.status(400).json({ message: 'Image error. Image with that URL already exists.' }); + return; + } + } + + // Check image URL already set as main image + if(product.productImage === req.body.imageUrl){ + res.status(400).json({ message: 'Image error. Image with that URL already exists.' }); + return; + } + + // Check productImages and init + if(!product.productImages){ + product.productImages = []; + } + // Add the image to our images + product.productImages.push(req.body.imageUrl); + + try{ + // if there isn't a product featured image, set this one + if(!product.productImage){ + await db.products.updateOne({ _id: getId(req.body.productId) }, { $set: { productImage: req.body.imageUrl } }, { multi: false }); + } + + // Add the images + await db.products.updateOne({ _id: getId(req.body.productId) }, { $set: { productImages: product.productImages } }, { multi: false }); + res.status(200).json({ message: 'Image added successfully' }); + }catch(ex){ + console.log('Failed to upload the file', ex); + res.status(400).json({ message: 'Image error. Please try again.' }); + } +}); + +// upload the file +// const upload = multer({ dest: 'public/uploads/' }); +const uploadFile=(async (req, res) => { + const db = req.app.db; + + if(req.file){ + const file = req.file; + + // Get the mime type of the file + const mimeType = mime.lookup(file.originalname); + + // Check for allowed mime type and file size + if(!allowedMimeType.includes(mimeType) || file.size > fileSizeLimit){ + // Remove temp file + fs.unlinkSync(file.path); + + // Return error + res.status(400).json({ message: 'File type not allowed or too large. Please try again.' }); + return; + } + + // get the product form the DB + const product = await db.products.findOne({ _id: getId(req.body.productId) }); + if(!product){ + // delete the temp file. + fs.unlinkSync(file.path); + + // Return error + res.status(400).json({ message: 'File upload error. Please try again.' }); + return; + } + + const productPath = product._id.toString(); + const uploadDir = path.join('public/uploads', productPath); + + // Check directory and create (if needed) + checkDirectorySync(uploadDir); + + // Setup the new path + const imagePath = path.join('/uploads', productPath, file.originalname.replace(/ /g, '_')); + + // save the new file + const dest = fs.createWriteStream(path.join(uploadDir, file.originalname.replace(/ /g, '_'))); + const pipeline = util.promisify(stream.pipeline); + + try{ + await pipeline( + fs.createReadStream(file.path), + dest + ); + + // delete the temp file. + fs.unlinkSync(file.path); + + // if there isn't a product featured image, set this one + if(!product.productImage){ + await db.products.updateOne({ _id: getId(req.body.productId) }, { $set: { productImage: imagePath } }, { multi: false }); + } + res.status(200).json({ message: 'File uploaded successfully' }); + }catch(ex){ + console.log('Failed to upload the file', ex); + res.status(400).json({ message: 'File upload error. Please try again.' }); + } + }else{ + // Return error + console.log('fail', req.file); + res.status(400).json({ message: 'File upload error. Please try again.' }); + } +}); + +// delete a file via ajax request +const testEmail=((req, res) => { + const config = req.app.config; + // TODO: Should fix this to properly handle result + sendEmail(config.emailAddress, 'expressCart test email', 'Your email settings are working'); + res.status(200).json({ message: 'Test email sent' }); +}); + +const searchAll=(async (req, res, next) => { + const db = req.app.db; + const searchValue = req.body.searchValue; + const limitReturned = 5; + + // Empty arrays + let customers = []; + let orders = []; + let products = []; + + // Default queries + const customerQuery = {}; + const orderQuery = {}; + const productQuery = {}; + + // If an ObjectId is detected use that + if(ObjectId.isValid(req.body.searchValue)){ + // Get customers + customers = await db.customers.find({ + _id: ObjectId(searchValue) + }) + .limit(limitReturned) + .sort({ created: 1 }) + .toArray(); + + // Get orders + orders = await db.orders.find({ + _id: ObjectId(searchValue) + }) + .limit(limitReturned) + .sort({ orderDate: 1 }) + .toArray(); + + // Get products + products = await db.products.find({ + _id: ObjectId(searchValue) + }) + .limit(limitReturned) + .sort({ productAddedDate: 1 }) + .toArray(); + + return res.status(200).json({ + customers, + orders, + products + }); + } + + // If email address is detected + if(emailRegex.test(req.body.searchValue)){ + customerQuery.email = searchValue; + orderQuery.orderEmail = searchValue; + }else if(numericRegex.test(req.body.searchValue)){ + // If a numeric value is detected + orderQuery.amount = req.body.searchValue; + productQuery.productPrice = req.body.searchValue; + }else{ + // String searches + customerQuery.$or = [ + { firstName: { $regex: new RegExp(searchValue, 'img') } }, + { lastName: { $regex: new RegExp(searchValue, 'img') } } + ]; + orderQuery.$or = [ + { orderFirstname: { $regex: new RegExp(searchValue, 'img') } }, + { orderLastname: { $regex: new RegExp(searchValue, 'img') } } + ]; + productQuery.$or = [ + { productTitle: { $regex: new RegExp(searchValue, 'img') } }, + { productDescription: { $regex: new RegExp(searchValue, 'img') } } + ]; + } + + // Get customers + if(Object.keys(customerQuery).length > 0){ + customers = await db.customers.find(customerQuery) + .limit(limitReturned) + .sort({ created: 1 }) + .toArray(); + } + + // Get orders + if(Object.keys(orderQuery).length > 0){ + orders = await db.orders.find(orderQuery) + .limit(limitReturned) + .sort({ orderDate: 1 }) + .toArray(); + } + + // Get products + if(Object.keys(productQuery).length > 0){ + products = await db.products.find(productQuery) + .limit(limitReturned) + .sort({ productAddedDate: 1 }) + .toArray(); + } + + return res.status(200).json({ + customers, + orders, + products + }); +}); + + + + +module.exports={adminDashboard, + settingsMenu, + logout, login, + loginValidate, + adminSetup, + setupUser, + dashboard, + getSettings, + genAPI, + settingsUpdate, + getPages, + newPages, + editPage, + insertPage, + deletePage, + menuItem, + updatemenu, + deletemenu, + saveOrder, + testEmail, + searchAll, + validatePerma, + discount, + editDiscount, + updateDiscount, + getDiscount, + createDiscount, + deleteDiscount, + addImage, + uploadFile, + + + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 04a21b2e..5e2e6a55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "eslint-plugin-standard": "^4.0.1", "globby": "^11.0.1", "less": "^3.12.2", + "nodemon": "^3.0.1", "pm2": "^4.5.0", "supertest": "^4.0.2", "uglify-es": "^3.3.9" @@ -604,6 +605,12 @@ "@types/node": "*" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -962,9 +969,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -974,15 +981,6 @@ "node": ">= 8" } }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -1356,18 +1354,6 @@ "node": ">=8" } }, - "node_modules/ava/node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/axios": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz", @@ -1808,24 +1794,30 @@ } }, "node_modules/chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "glob-parent": "~5.1.0", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "readdirp": "~3.6.0" }, "engines": { "node": ">= 8.10.0" }, "optionalDependencies": { - "fsevents": "~2.1.2" + "fsevents": "~2.3.2" } }, "node_modules/chunkd": { @@ -3869,18 +3861,6 @@ "node": ">=8" } }, - "node_modules/fast-glob/node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/fast-glob/node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4120,10 +4100,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "node_modules/fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "deprecated": "\"Please update to latest v2.3 or v2.2\"", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -5555,6 +5534,18 @@ "node": ">=0.10.0" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/lunr": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", @@ -5817,9 +5808,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6105,6 +6096,97 @@ "node": ">=6.0.0" } }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -6771,12 +6853,15 @@ } }, "node_modules/picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pidusage": { @@ -7125,27 +7210,6 @@ "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", "dev": true }, - "node_modules/pm2/node_modules/binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pm2/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/pm2/node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -7159,27 +7223,6 @@ "node": ">=8" } }, - "node_modules/pm2/node_modules/chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.1.2" - } - }, "node_modules/pm2/node_modules/commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", @@ -7204,54 +7247,6 @@ } } }, - "node_modules/pm2/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pm2/node_modules/fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "deprecated": "\"Please update to latest v2.3 or v2.2\"", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/pm2/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pm2/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/pm2/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -7270,39 +7265,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/pm2/node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pm2/node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pm2/node_modules/readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/pm2/node_modules/semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -7331,18 +7293,6 @@ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "dev": true }, - "node_modules/pm2/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/postcss": { "version": "8.2.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.14.tgz", @@ -7515,6 +7465,12 @@ "node": ">=8" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -7763,9 +7719,9 @@ } }, "node_modules/readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "dependencies": { "picomatch": "^2.2.1" @@ -7774,18 +7730,6 @@ "node": ">=8.10.0" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/referrer-policy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", @@ -8309,6 +8253,33 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sitemap": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-1.13.0.tgz", @@ -8966,6 +8937,18 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/trim-off-newlines": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", @@ -9128,6 +9111,12 @@ "node": ">= 0.8" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/underscore": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", @@ -9494,6 +9483,12 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/yamljs": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", @@ -10198,6 +10193,12 @@ "@types/node": "*" } }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -10485,21 +10486,13 @@ "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" - }, - "dependencies": { - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - } } }, "append-field": { @@ -10795,12 +10788,6 @@ "requires": { "p-finally": "^1.0.0" } - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true } } }, @@ -11168,19 +11155,19 @@ } }, "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "readdirp": "~3.6.0" } }, "chunkd": { @@ -12809,12 +12796,6 @@ "picomatch": "^2.0.5" } }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13001,9 +12982,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "optional": true }, @@ -14151,6 +14132,15 @@ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "lunr": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", @@ -14350,9 +14340,9 @@ "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } @@ -14579,6 +14569,74 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.16.tgz", "integrity": "sha512-68K0LgZ6hmZ7PVmwL78gzNdjpj5viqBdFqKrTtr9bZbJYj6BRj5W6WGkxXrEnUl3Co3CBXi3CZBUlpV/foGnOQ==" }, + "nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -15093,9 +15151,9 @@ } }, "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pidusage": { @@ -15276,21 +15334,6 @@ "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", "dev": true }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -15301,22 +15344,6 @@ "supports-color": "^7.1.0" } }, - "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" - } - }, "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", @@ -15332,37 +15359,6 @@ "ms": "2.1.2" } }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -15375,27 +15371,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -15417,15 +15392,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } } } }, @@ -15643,6 +15609,12 @@ "integrity": "sha512-qau0czUSB0fzSlBOQt0bo+I2v6R+xiQdj78e1BR/Qjfl5OHWJ/urXi8+ilw1eHe+5hSeDI1wrwVTgDp2wst4oA==", "dev": true }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -15841,20 +15813,12 @@ } }, "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { "picomatch": "^2.2.1" - }, - "dependencies": { - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - } } }, "referrer-policy": { @@ -16261,6 +16225,26 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "sitemap": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-1.13.0.tgz", @@ -16777,6 +16761,15 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, "trim-off-newlines": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", @@ -16900,6 +16893,12 @@ "random-bytes": "~1.0.0" } }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "underscore": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", @@ -17185,6 +17184,12 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yamljs": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", diff --git a/package.json b/package.json index ecdb4106..480df070 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A fully functioning Node.js shopping cart with Stripe, PayPal and Authorize.net payments.", "private": false, "scripts": { - "start": "node app.js", + "start": "nodemon app.js", "deploy": "node deploy.js", "testdata": "node lib/testdata.js", "test": "ava", @@ -88,6 +88,7 @@ "eslint-plugin-standard": "^4.0.1", "globby": "^11.0.1", "less": "^3.12.2", + "nodemon": "^3.0.1", "pm2": "^4.5.0", "supertest": "^4.0.2", "uglify-es": "^3.3.9" diff --git a/routes/admin.js b/routes/admin.js index 0cfe9866..a1ba6a40 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1,6 +1,5 @@ const express = require('express'); const { restrict, checkAccess } = require('../lib/auth'); -const escape = require('html-entities').AllHtmlEntities; const colors = require('colors'); const bcrypt = require('bcryptjs'); const moment = require('moment'); @@ -11,21 +10,11 @@ const mime = require('mime-type/with-db'); const csrf = require('csurf'); const util = require('util'); const stream = require('stream'); -const { validateJson } = require('../lib/schema'); -const { - clearSessionValue, - mongoSanitize, - getThemes, - getId, - allowedMimeType, - fileSizeLimit, - checkDirectorySync, - sendEmail -} = require('../lib/common'); -const { - getConfig, - updateConfig -} = require('../lib/config'); + + +const {adminDashboard,logout,login, loginValidate, adminSetup, setupUser, dashboard, getSettings, genAPI, settingsUpdate, settingsMenu, getPages, newPages, editPage, insertPage, deletePage, menuItem, saveOrder, validatePerma, discount, editDiscount, updateDiscount, getDiscount, createDiscount, deleteDiscount, addImage, uploadFile, testEmail, searchAll} = require('../controller/admin.controller') + + const { sortMenu, getMenu, @@ -34,7 +23,7 @@ const { deleteMenu, orderMenu } = require('../lib/menu'); -const ObjectId = require('mongodb').ObjectID; + const router = express.Router(); const csrfProtection = csrf({ cookie: true }); @@ -43,17 +32,10 @@ const emailRegex = /\S+@\S+\.\S+/; const numericRegex = /^\d*\.?\d*$/; // Admin section -router.get('/admin', restrict, (req, res, next) => { - res.redirect('/admin/dashboard'); -}); +router.get('/admin',adminDashboard); // logout -router.get('/admin/logout', (req, res) => { - req.session.user = null; - req.session.message = null; - req.session.messageType = null; - res.redirect('/'); -}); +router.get('/admin/logout',logout); // Used for tests only if(process.env.NODE_ENV === 'test'){ @@ -65,805 +47,90 @@ if(process.env.NODE_ENV === 'test'){ } // login form -router.get('/admin/login', async (req, res) => { - const db = req.app.db; - - const userCount = await db.users.countDocuments({}); - // we check for a user. If one exists, redirect to login form otherwise setup - if(userCount && userCount > 0){ - // set needsSetup to false as a user exists - req.session.needsSetup = false; - res.render('login', { - title: 'Login', - referringUrl: req.header('Referer'), - config: req.app.config, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - showFooter: 'showFooter' - }); - }else{ - // if there are no users set the "needsSetup" session - req.session.needsSetup = true; - res.redirect('/admin/setup'); - } -}); +router.get('/admin/login', login ); // login the user and check the password -router.post('/admin/login_action', async (req, res) => { - const db = req.app.db; - - const user = await db.users.findOne({ userEmail: mongoSanitize(req.body.email) }); - if(!user || user === null){ - res.status(400).json({ message: 'A user with that email does not exist.' }); - return; - } - - // we have a user under that email so we compare the password - bcrypt.compare(req.body.password, user.userPassword) - .then((result) => { - if(result){ - req.session.user = req.body.email; - req.session.usersName = user.usersName; - req.session.userId = user._id.toString(); - req.session.isAdmin = user.isAdmin; - res.status(200).json({ message: 'Login successful' }); - return; - } - // password is not correct - res.status(400).json({ message: 'Access denied. Check password and try again.' }); - }); -}); +router.post('/admin/login_action', loginValidate); // setup form is shown when there are no users setup in the DB -router.get('/admin/setup', async (req, res) => { - const db = req.app.db; - - const userCount = await db.users.countDocuments({}); - // dont allow the user to "re-setup" if a user exists. - // set needsSetup to false as a user exists - req.session.needsSetup = false; - if(userCount === 0){ - req.session.needsSetup = true; - res.render('setup', { - title: 'Setup', - config: req.app.config, - helpers: req.handlebars.helpers, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - showFooter: 'showFooter' - }); - return; - } - res.redirect('/admin/login'); -}); +router.get('/admin/setup', adminSetup); // insert a user -router.post('/admin/setup_action', async (req, res) => { - const db = req.app.db; - - const doc = { - usersName: req.body.usersName, - userEmail: req.body.userEmail, - userPassword: bcrypt.hashSync(req.body.userPassword, 10), - isAdmin: true, - isOwner: true - }; - - // check for users - const userCount = await db.users.countDocuments({}); - if(userCount === 0){ - // email is ok to be used. - try{ - await db.users.insertOne(doc); - res.status(200).json({ message: 'User account inserted' }); - return; - }catch(ex){ - console.error(colors.red(`Failed to insert user: ${ex}`)); - res.status(200).json({ message: 'Setup failed' }); - return; - } - } - res.status(200).json({ message: 'Already setup.' }); -}); +router.post('/admin/setup_action', setupUser); // dashboard -router.get('/admin/dashboard', csrfProtection, restrict, async (req, res) => { - const db = req.app.db; - - // Collate data for dashboard - const dashboardData = { - productsCount: await db.products.countDocuments({ - productPublished: true - }), - ordersCount: await db.orders.countDocuments({}), - ordersAmount: await db.orders.aggregate([{ $match: {} }, - { $group: { _id: null, sum: { $sum: '$orderTotal' } } - }]).toArray(), - productsSold: await db.orders.aggregate([{ $match: {} }, - { $group: { _id: null, sum: { $sum: '$orderProductCount' } } - }]).toArray(), - topProducts: await db.orders.aggregate([ - { $project: { _id: 0 } }, - { $project: { o: { $objectToArray: '$orderProducts' } } }, - { $unwind: '$o' }, - { $group: { - _id: '$o.v.title', - productImage: { $last: '$o.v.productImage' }, - count: { $sum: '$o.v.quantity' } - } }, - { $sort: { count: -1 } }, - { $limit: 5 } - ]).toArray() - }; - - // Fix aggregate data - if(dashboardData.ordersAmount.length > 0){ - dashboardData.ordersAmount = dashboardData.ordersAmount[0].sum; - } - if(dashboardData.productsSold.length > 0){ - dashboardData.productsSold = dashboardData.productsSold[0].sum; - }else{ - dashboardData.productsSold = 0; - } - - res.render('dashboard', { - title: 'Cart dashboard', - session: req.session, - admin: true, - dashboardData, - themes: getThemes(), - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/dashboard', csrfProtection, restrict, dashboard); // settings -router.get('/admin/settings', csrfProtection, restrict, (req, res) => { - res.render('settings', { - title: 'Cart settings', - session: req.session, - admin: true, - themes: getThemes(), - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - footerHtml: typeof req.app.config.footerHtml !== 'undefined' ? escape.decode(req.app.config.footerHtml) : null, - googleAnalytics: typeof req.app.config.googleAnalytics !== 'undefined' ? escape.decode(req.app.config.googleAnalytics) : null, - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings', csrfProtection, restrict, getSettings); // create API key -router.post('/admin/createApiKey', restrict, checkAccess, async (req, res) => { - const db = req.app.db; - const result = await db.users.findOneAndUpdate({ - _id: ObjectId(req.session.userId), - isAdmin: true - }, { - $set: { - apiKey: new ObjectId() - } - }, { - returnOriginal: false - }); - - if(result.value && result.value.apiKey){ - res.status(200).json({ message: 'API Key generated', apiKey: result.value.apiKey }); - return; - } - res.status(400).json({ message: 'Failed to generate API Key' }); -}); +router.post('/admin/createApiKey', restrict, checkAccess, genAPI); // settings update -router.post('/admin/settings/update', restrict, checkAccess, (req, res) => { - const result = updateConfig(req.body); - if(result === true){ - req.app.config = getConfig(); - res.status(200).json({ message: 'Settings successfully updated' }); - return; - } - res.status(400).json({ message: 'Permission denied' }); -}); +router.post('/admin/settings/update', restrict, checkAccess, settingsUpdate); // settings menu -router.get('/admin/settings/menu', csrfProtection, restrict, async (req, res) => { - const db = req.app.db; - res.render('settings-menu', { - title: 'Cart menu', - session: req.session, - admin: true, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - menu: sortMenu(await getMenu(db)), - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings/menu', csrfProtection, restrict, settingsMenu); // page list -router.get('/admin/settings/pages', csrfProtection, restrict, async (req, res) => { - const db = req.app.db; - const pages = await db.pages.find({}).toArray(); - - res.render('settings-pages', { - title: 'Static pages', - pages: pages, - session: req.session, - admin: true, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - menu: sortMenu(await getMenu(db)), - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings/pages', csrfProtection, restrict, getPages); // pages new -router.get('/admin/settings/pages/new', csrfProtection, restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - res.render('settings-page', { - title: 'Static pages', - session: req.session, - admin: true, - button_text: 'Create', - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - menu: sortMenu(await getMenu(db)), - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings/pages/new', csrfProtection, restrict, checkAccess, newPages); // pages editor -router.get('/admin/settings/pages/edit/:page', csrfProtection, restrict, checkAccess, async (req, res) => { - const db = req.app.db; - const page = await db.pages.findOne({ _id: getId(req.params.page) }); - const menu = sortMenu(await getMenu(db)); - if(!page){ - res.status(404).render('error', { - title: '404 Error - Page not found', - config: req.app.config, - message: '404 Error - Page not found', - helpers: req.handlebars.helpers, - showFooter: 'showFooter', - menu - }); - return; - } - - res.render('settings-page', { - title: 'Static pages', - page: page, - button_text: 'Update', - session: req.session, - admin: true, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - menu, - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings/pages/edit/:page', csrfProtection, restrict, checkAccess, editPage); // insert/update page -router.post('/admin/settings/page', restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - const doc = { - pageName: req.body.pageName, - pageSlug: req.body.pageSlug, - pageEnabled: req.body.pageEnabled, - pageContent: req.body.pageContent - }; - - if(req.body.pageId){ - // existing page - const page = await db.pages.findOne({ _id: getId(req.body.pageId) }); - if(!page){ - res.status(400).json({ message: 'Page not found' }); - return; - } - - try{ - const updatedPage = await db.pages.findOneAndUpdate({ _id: getId(req.body.pageId) }, { $set: doc }, { returnOriginal: false }); - res.status(200).json({ message: 'Page updated successfully', pageId: req.body.pageId, page: updatedPage.value }); - }catch(ex){ - res.status(400).json({ message: 'Error updating page. Please try again.' }); - } - }else{ - // insert page - try{ - const newDoc = await db.pages.insertOne(doc); - res.status(200).json({ message: 'New page successfully created', pageId: newDoc.insertedId }); - return; - }catch(ex){ - res.status(400).json({ message: 'Error creating page. Please try again.' }); - } - } -}); +router.post('/admin/settings/page', restrict, checkAccess, insertPage); // delete a page -router.post('/admin/settings/page/delete', restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - const page = await db.pages.findOne({ _id: getId(req.body.pageId) }); - if(!page){ - res.status(400).json({ message: 'Page not found' }); - return; - } - - try{ - await db.pages.deleteOne({ _id: getId(req.body.pageId) }, {}); - res.status(200).json({ message: 'Page successfully deleted' }); - return; - }catch(ex){ - res.status(400).json({ message: 'Error deleting page. Please try again.' }); - } -}); +router.post('/admin/settings/page/delete', restrict, checkAccess, deletePage); // new menu item -router.post('/admin/settings/menu/new', restrict, checkAccess, (req, res) => { - const result = newMenu(req); - if(result === false){ - res.status(400).json({ message: 'Failed creating menu.' }); - return; - } - res.status(200).json({ message: 'Menu created successfully.' }); -}); +router.post('/admin/settings/menu/new', restrict, checkAccess, menuItem); // update existing menu item -router.post('/admin/settings/menu/update', restrict, checkAccess, (req, res) => { - const result = updateMenu(req); - if(result === false){ - res.status(400).json({ message: 'Failed updating menu.' }); - return; - } - res.status(200).json({ message: 'Menu updated successfully.' }); -}); +router.post('/admin/settings/menu/update', restrict, checkAccess, updateMenu); // delete menu item -router.post('/admin/settings/menu/delete', restrict, checkAccess, (req, res) => { - const result = deleteMenu(req, req.body.menuId); - if(result === false){ - res.status(400).json({ message: 'Failed deleting menu.' }); - return; - } - res.status(200).json({ message: 'Menu deleted successfully.' }); -}); - -// We call this via a Ajax call to save the order from the sortable list -router.post('/admin/settings/menu/saveOrder', restrict, checkAccess, (req, res) => { - const result = orderMenu(req, res); - if(result === false){ - res.status(400).json({ message: 'Failed saving menu order' }); - return; - } - res.status(200).json({}); -}); +router.post('/admin/settings/menu/delete', restrict, checkAccess, deleteMenu); + +// We call this via a Ajax call to save the order from the sortable list +router.post('/admin/settings/menu/saveOrder', restrict, checkAccess, saveOrder); // validate the permalink -router.post('/admin/validatePermalink', async (req, res) => { - // if doc id is provided it checks for permalink in any products other that one provided, - // else it just checks for any products with that permalink - const db = req.app.db; - - let query = {}; - if(typeof req.body.docId === 'undefined' || req.body.docId === ''){ - query = { productPermalink: req.body.permalink }; - }else{ - query = { productPermalink: req.body.permalink, _id: { $ne: getId(req.body.docId) } }; - } - - const products = await db.products.countDocuments(query); - if(products && products > 0){ - res.status(400).json({ message: 'Permalink already exists' }); - return; - } - res.status(200).json({ message: 'Permalink validated successfully' }); -}); +router.post('/admin/validatePermalink', validatePerma); // Discount codes -router.get('/admin/settings/discounts', csrfProtection, restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - const discounts = await db.discounts.find({}).toArray(); - - res.render('settings-discounts', { - title: 'Discount code', - config: req.app.config, - session: req.session, - discounts, - admin: true, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings/discounts', csrfProtection, restrict, checkAccess, discount); // Edit a discount code -router.get('/admin/settings/discount/edit/:id', csrfProtection, restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - const discount = await db.discounts.findOne({ _id: getId(req.params.id) }); - - res.render('settings-discount-edit', { - title: 'Discount code edit', - session: req.session, - admin: true, - discount, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings/discount/edit/:id', csrfProtection, restrict, checkAccess, editDiscount); // Update discount code -router.post('/admin/settings/discount/update', restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - // Doc to insert - const discountDoc = { - discountId: req.body.discountId, - code: req.body.code, - type: req.body.type, - value: parseInt(req.body.value), - start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(), - end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate() - }; - - // Validate the body again schema - const schemaValidate = validateJson('editDiscount', discountDoc); - if(!schemaValidate.result){ - res.status(400).json(schemaValidate.errors); - return; - } - - // Check start is after today - if(moment(discountDoc.start).isBefore(moment())){ - res.status(400).json({ message: 'Discount start date needs to be after today' }); - return; - } - - // Check end is after the start - if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){ - res.status(400).json({ message: 'Discount end date needs to be after start date' }); - return; - } - - // Check if code exists - const checkCode = await db.discounts.countDocuments({ - code: discountDoc.code, - _id: { $ne: getId(discountDoc.discountId) } - }); - if(checkCode){ - res.status(400).json({ message: 'Discount code already exists' }); - return; - } - - // Remove discountID - delete discountDoc.discountId; - - try{ - await db.discounts.updateOne({ _id: getId(req.body.discountId) }, { $set: discountDoc }, {}); - res.status(200).json({ message: 'Successfully saved', discount: discountDoc }); - }catch(ex){ - res.status(400).json({ message: 'Failed to save. Please try again' }); - } -}); +router.post('/admin/settings/discount/update', restrict, checkAccess, updateDiscount); // Create a discount code -router.get('/admin/settings/discount/new', csrfProtection, restrict, checkAccess, async (req, res) => { - res.render('settings-discount-new', { - title: 'Discount code create', - session: req.session, - admin: true, - message: clearSessionValue(req.session, 'message'), - messageType: clearSessionValue(req.session, 'messageType'), - helpers: req.handlebars.helpers, - config: req.app.config, - csrfToken: req.csrfToken() - }); -}); +router.get('/admin/settings/discount/new', csrfProtection, restrict, checkAccess, getDiscount); // Create a discount code -router.post('/admin/settings/discount/create', csrfProtection, restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - // Doc to insert - const discountDoc = { - code: req.body.code, - type: req.body.type, - value: parseInt(req.body.value), - start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(), - end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate() - }; - - // Validate the body again schema - const schemaValidate = validateJson('newDiscount', discountDoc); - if(!schemaValidate.result){ - res.status(400).json(schemaValidate.errors); - return; - } - - // Check if code exists - const checkCode = await db.discounts.countDocuments({ - code: discountDoc.code - }); - if(checkCode){ - res.status(400).json({ message: 'Discount code already exists' }); - return; - } - - // Check start is after today - if(moment(discountDoc.start).isBefore(moment())){ - res.status(400).json({ message: 'Discount start date needs to be after today' }); - return; - } - - // Check end is after the start - if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){ - res.status(400).json({ message: 'Discount end date needs to be after start date' }); - return; - } - - // Insert discount code - const discount = await db.discounts.insertOne(discountDoc); - res.status(200).json({ message: 'Discount code created successfully', discountId: discount.insertedId }); -}); +router.post('/admin/settings/discount/create', csrfProtection, restrict, checkAccess, createDiscount); // Delete discount code -router.delete('/admin/settings/discount/delete', restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - try{ - await db.discounts.deleteOne({ _id: getId(req.body.discountId) }, {}); - res.status(200).json({ message: 'Discount code successfully deleted' }); - return; - }catch(ex){ - res.status(400).json({ message: 'Error deleting discount code. Please try again.' }); - } -}); +router.delete('/admin/settings/discount/delete', restrict, checkAccess, deleteDiscount); // Add image by URL -router.post('/admin/file/url', restrict, checkAccess, async (req, res) => { - const db = req.app.db; - - // get the product form the DB - const product = await db.products.findOne({ _id: getId(req.body.productId) }); - if(!product){ - // Return error - res.status(400).json({ message: 'Image error. Please try again.' }); - return; - } - - // Check image URL already in list - if(product.productImages){ - if(product.productImages.includes(req.body.imageUrl)){ - res.status(400).json({ message: 'Image error. Image with that URL already exists.' }); - return; - } - } - - // Check image URL already set as main image - if(product.productImage === req.body.imageUrl){ - res.status(400).json({ message: 'Image error. Image with that URL already exists.' }); - return; - } - - // Check productImages and init - if(!product.productImages){ - product.productImages = []; - } - // Add the image to our images - product.productImages.push(req.body.imageUrl); - - try{ - // if there isn't a product featured image, set this one - if(!product.productImage){ - await db.products.updateOne({ _id: getId(req.body.productId) }, { $set: { productImage: req.body.imageUrl } }, { multi: false }); - } - - // Add the images - await db.products.updateOne({ _id: getId(req.body.productId) }, { $set: { productImages: product.productImages } }, { multi: false }); - res.status(200).json({ message: 'Image added successfully' }); - }catch(ex){ - console.log('Failed to upload the file', ex); - res.status(400).json({ message: 'Image error. Please try again.' }); - } -}); +router.post('/admin/file/url', restrict, checkAccess, addImage); // upload the file const upload = multer({ dest: 'public/uploads/' }); -router.post('/admin/file/upload', restrict, checkAccess, upload.single('uploadFile'), async (req, res) => { - const db = req.app.db; - - if(req.file){ - const file = req.file; - - // Get the mime type of the file - const mimeType = mime.lookup(file.originalname); - - // Check for allowed mime type and file size - if(!allowedMimeType.includes(mimeType) || file.size > fileSizeLimit){ - // Remove temp file - fs.unlinkSync(file.path); - - // Return error - res.status(400).json({ message: 'File type not allowed or too large. Please try again.' }); - return; - } - - // get the product form the DB - const product = await db.products.findOne({ _id: getId(req.body.productId) }); - if(!product){ - // delete the temp file. - fs.unlinkSync(file.path); - - // Return error - res.status(400).json({ message: 'File upload error. Please try again.' }); - return; - } - - const productPath = product._id.toString(); - const uploadDir = path.join('public/uploads', productPath); - - // Check directory and create (if needed) - checkDirectorySync(uploadDir); - - // Setup the new path - const imagePath = path.join('/uploads', productPath, file.originalname.replace(/ /g, '_')); - - // save the new file - const dest = fs.createWriteStream(path.join(uploadDir, file.originalname.replace(/ /g, '_'))); - const pipeline = util.promisify(stream.pipeline); - - try{ - await pipeline( - fs.createReadStream(file.path), - dest - ); - - // delete the temp file. - fs.unlinkSync(file.path); - - // if there isn't a product featured image, set this one - if(!product.productImage){ - await db.products.updateOne({ _id: getId(req.body.productId) }, { $set: { productImage: imagePath } }, { multi: false }); - } - res.status(200).json({ message: 'File uploaded successfully' }); - }catch(ex){ - console.log('Failed to upload the file', ex); - res.status(400).json({ message: 'File upload error. Please try again.' }); - } - }else{ - // Return error - console.log('fail', req.file); - res.status(400).json({ message: 'File upload error. Please try again.' }); - } -}); +router.post('/admin/file/upload', restrict, checkAccess, upload.single('uploadFile'), uploadFile); // delete a file via ajax request -router.post('/admin/testEmail', restrict, (req, res) => { - const config = req.app.config; - // TODO: Should fix this to properly handle result - sendEmail(config.emailAddress, 'expressCart test email', 'Your email settings are working'); - res.status(200).json({ message: 'Test email sent' }); -}); - -router.post('/admin/searchall', restrict, async (req, res, next) => { - const db = req.app.db; - const searchValue = req.body.searchValue; - const limitReturned = 5; - - // Empty arrays - let customers = []; - let orders = []; - let products = []; - - // Default queries - const customerQuery = {}; - const orderQuery = {}; - const productQuery = {}; - - // If an ObjectId is detected use that - if(ObjectId.isValid(req.body.searchValue)){ - // Get customers - customers = await db.customers.find({ - _id: ObjectId(searchValue) - }) - .limit(limitReturned) - .sort({ created: 1 }) - .toArray(); - - // Get orders - orders = await db.orders.find({ - _id: ObjectId(searchValue) - }) - .limit(limitReturned) - .sort({ orderDate: 1 }) - .toArray(); - - // Get products - products = await db.products.find({ - _id: ObjectId(searchValue) - }) - .limit(limitReturned) - .sort({ productAddedDate: 1 }) - .toArray(); - - return res.status(200).json({ - customers, - orders, - products - }); - } - - // If email address is detected - if(emailRegex.test(req.body.searchValue)){ - customerQuery.email = searchValue; - orderQuery.orderEmail = searchValue; - }else if(numericRegex.test(req.body.searchValue)){ - // If a numeric value is detected - orderQuery.amount = req.body.searchValue; - productQuery.productPrice = req.body.searchValue; - }else{ - // String searches - customerQuery.$or = [ - { firstName: { $regex: new RegExp(searchValue, 'img') } }, - { lastName: { $regex: new RegExp(searchValue, 'img') } } - ]; - orderQuery.$or = [ - { orderFirstname: { $regex: new RegExp(searchValue, 'img') } }, - { orderLastname: { $regex: new RegExp(searchValue, 'img') } } - ]; - productQuery.$or = [ - { productTitle: { $regex: new RegExp(searchValue, 'img') } }, - { productDescription: { $regex: new RegExp(searchValue, 'img') } } - ]; - } - - // Get customers - if(Object.keys(customerQuery).length > 0){ - customers = await db.customers.find(customerQuery) - .limit(limitReturned) - .sort({ created: 1 }) - .toArray(); - } - - // Get orders - if(Object.keys(orderQuery).length > 0){ - orders = await db.orders.find(orderQuery) - .limit(limitReturned) - .sort({ orderDate: 1 }) - .toArray(); - } - - // Get products - if(Object.keys(productQuery).length > 0){ - products = await db.products.find(productQuery) - .limit(limitReturned) - .sort({ productAddedDate: 1 }) - .toArray(); - } - - return res.status(200).json({ - customers, - orders, - products - }); -}); +router.post('/admin/testEmail', restrict, testEmail); + +router.post('/admin/searchall', restrict, searchAll); -module.exports = router; +module.exports = router; \ No newline at end of file