Started adding some tests

master
Mark Moffat 2018-02-22 18:41:24 +01:00
parent 245c67188b
commit 61c59c8997
18 changed files with 4246 additions and 145 deletions

15
app.js
View File

@ -315,7 +315,13 @@ MongoClient.connect(config.databaseConnectionString, {}, (err, client) => {
// select DB // select DB
const dbUriObj = mongodbUri.parse(config.databaseConnectionString); const dbUriObj = mongodbUri.parse(config.databaseConnectionString);
const db = client.db(dbUriObj.database); let db;
// if in testing, set the testing DB
if(process.env.NODE_ENV === 'test'){
db = client.db('testingdb');
}else{
db = client.db(dbUriObj.database);
}
// setup the collections // setup the collections
db.users = db.collection('users'); db.users = db.collection('users');
@ -326,17 +332,20 @@ MongoClient.connect(config.databaseConnectionString, {}, (err, client) => {
db.customers = db.collection('customers'); db.customers = db.collection('customers');
// add db to app for routes // add db to app for routes
app.dbClient = client;
app.db = db; app.db = db;
app.config = config;
app.port = app.get('port');
// run indexing // run indexing
common.runIndexing(app) common.runIndexing(app)
.then(common.testData(db, app))
.then(app.listen(app.get('port'))) .then(app.listen(app.get('port')))
.then(() => { .then(() => {
// lift the app // lift the app
app.emit('appStarted');
console.log(colors.green('expressCart running on host: http://localhost:' + app.get('port'))); console.log(colors.green('expressCart running on host: http://localhost:' + app.get('port')));
}) })
.catch(() => { .catch((err) => {
console.error(colors.red('Error setting up indexes:' + err)); console.error(colors.red('Error setting up indexes:' + err));
process.exit(2); process.exit(2);
}); });

View File

@ -14,7 +14,7 @@
"productImage": "/uploads/duckworth-jacket/woolfill-jacket_6c39ae23-c0c8-4821-85f4-4b5d64333c62_grande.jpg" "productImage": "/uploads/duckworth-jacket/woolfill-jacket_6c39ae23-c0c8-4821-85f4-4b5d64333c62_grande.jpg"
}, },
{ {
"productPermalink": "5-panel-cap", "productPermalink": "5-panel-camp-cap",
"productTitle": "5 Panel Camp Cap", "productTitle": "5 Panel Camp Cap",
"productPrice": "48", "productPrice": "48",
"productDescription": "<p style=\"margin-bottom: 25px; text-rendering: optimizeLegibility;\">A classic 5 panel hat with our United By Blue logo on the front and an adjustable strap to keep fit and secure. Made with recycled polyester and organic cotton mix.<\/p><ul style=\"margin-right: 0px; margin-bottom: 25px; margin-left: 20px; padding: 0px; text-rendering: optimizeLegibility;\"><li style=\"margin-bottom: 0px;\">Made in&nbsp;New Jersey<\/li><li style=\"margin-bottom: 0px;\">7oz Eco-Twill fabric:&nbsp;35% organic cotton, 65% recycled PET&nbsp;(plastic water and soda bottles)&nbsp;<\/li><li style=\"margin-bottom: 0px;\">Embossed leather patch<\/li><\/ul><ul class=\"tabs\" style=\"margin-right: 0px; margin-bottom: 25px; margin-left: 20px; padding: 0px; text-rendering: optimizeLegibility; color: rgb(28, 29, 29); font-family: Arapey, serif; line-height: 25.008px;\"><\/ul>", "productDescription": "<p style=\"margin-bottom: 25px; text-rendering: optimizeLegibility;\">A classic 5 panel hat with our United By Blue logo on the front and an adjustable strap to keep fit and secure. Made with recycled polyester and organic cotton mix.<\/p><ul style=\"margin-right: 0px; margin-bottom: 25px; margin-left: 20px; padding: 0px; text-rendering: optimizeLegibility;\"><li style=\"margin-bottom: 0px;\">Made in&nbsp;New Jersey<\/li><li style=\"margin-bottom: 0px;\">7oz Eco-Twill fabric:&nbsp;35% organic cotton, 65% recycled PET&nbsp;(plastic water and soda bottles)&nbsp;<\/li><li style=\"margin-bottom: 0px;\">Embossed leather patch<\/li><\/ul><ul class=\"tabs\" style=\"margin-right: 0px; margin-bottom: 25px; margin-left: 20px; padding: 0px; text-rendering: optimizeLegibility; color: rgb(28, 29, 29); font-family: Arapey, serif; line-height: 25.008px;\"><\/ul>",
@ -79,7 +79,7 @@
"productImage" : "/uploads/hudderton-backpack/hudderton-backpack_dc8afb13-448b-49d9-a042-5a163a97de8f_590x.jpg" "productImage" : "/uploads/hudderton-backpack/hudderton-backpack_dc8afb13-448b-49d9-a042-5a163a97de8f_590x.jpg"
}, },
{ {
"productPermalink" : "Ayres", "productPermalink" : "ayres-chambray",
"productTitle" : "Ayres Chambray", "productTitle" : "Ayres Chambray",
"productPrice" : "77", "productPrice" : "77",
"productDescription" : "<p style=\"margin-bottom: 25px; text-rendering: optimizeLegibility; color: rgb(34, 35, 35); font-family: Arapey, serif;\">Comfortable and practical, our chambray button down is perfect for travel or days spent on the go. The Ayres Chambray has a rich, washed out indigo color suitable to throw on for any event. Made with sustainable soft chambray featuring two chest pockets with sturdy and scratch resistant corozo buttons.</p><ul class=\"tabs-content\" style=\"margin-right: 0px; margin-bottom: 25px; margin-left: 20px; padding: 0px; text-rendering: optimizeLegibility; color: rgb(34, 35, 35); font-family: Arapey, serif;\"><li style=\"margin-bottom: 0px;\"><span style=\"line-height: 1.4;\">100% Organic Cotton Chambray, 4.9 oz Fabric.</span></li><li style=\"margin-bottom: 0px;\"><span style=\"line-height: 1.4;\">Natural Corozo Buttons.</span></li></ul>", "productDescription" : "<p style=\"margin-bottom: 25px; text-rendering: optimizeLegibility; color: rgb(34, 35, 35); font-family: Arapey, serif;\">Comfortable and practical, our chambray button down is perfect for travel or days spent on the go. The Ayres Chambray has a rich, washed out indigo color suitable to throw on for any event. Made with sustainable soft chambray featuring two chest pockets with sturdy and scratch resistant corozo buttons.</p><ul class=\"tabs-content\" style=\"margin-right: 0px; margin-bottom: 25px; margin-left: 20px; padding: 0px; text-rendering: optimizeLegibility; color: rgb(34, 35, 35); font-family: Arapey, serif;\"><li style=\"margin-bottom: 0px;\"><span style=\"line-height: 1.4;\">100% Organic Cotton Chambray, 4.9 oz Fabric.</span></li><li style=\"margin-bottom: 0px;\"><span style=\"line-height: 1.4;\">Natural Corozo Buttons.</span></li></ul>",
@ -92,6 +92,28 @@
"productImage" : "/uploads/Ayres Chambray/chambray_5f232530-4331-492a-872c-81c225d6bafd_590x.jpg" "productImage" : "/uploads/Ayres Chambray/chambray_5f232530-4331-492a-872c-81c225d6bafd_590x.jpg"
} }
], ],
"customers": [
{
"email" : "test@test.com",
"firstName" : "Testy",
"lastName" : "Cles",
"address1" : "1 test street",
"address2" : "testvile",
"country" : "Netherlands",
"state" : "",
"postcode" : "2000TW",
"phone" : "123456789",
"password" : "$2a$10$kKjnX1J/CAdgdmLI0WuPY.ILH1c7N8mD0H/ZyUXEfee1mJxJvZIS."
}
],
"users": [
{
"usersName" : "test",
"userEmail" : "test@test.com",
"userPassword" : "$2a$10$7jQx/hQOWrRni531b/dHRuH8o1ZP8Yo8g..GpTOF4M7RrEH/pzTMy",
"isAdmin" : "false"
}
],
"menu": { "menu": {
"items": [ "items": [
{ {

View File

@ -600,25 +600,69 @@ exports.runIndexing = (app) => {
}); });
}; };
exports.testData = (db, app) => { exports.dropTestData = (db) => {
db.products.count({}) Promise.all([
db.products.drop(),
db.users.drop(),
db.customers.drop()
])
.then((err) => {
return Promise.resolve();
})
.catch((err) => {
console.log('Error dropping test data', err);
});
};
exports.sampleData = (app) => {
const db = app.db;
db.products.count()
.then((products) => { .then((products) => {
if(products > 0){ if(products !== 0){
return Promise.resolve(); return Promise.resolve();
} }
console.info(colors.cyan('No products, inserting test data')); console.log('Inserting sample data');
const testData = fs.readFileSync('./bin/testdata.json', 'utf-8'); const testData = fs.readFileSync('./bin/testdata.json', 'utf-8');
const jsonData = JSON.parse(testData); const jsonData = JSON.parse(testData);
// Add sample data
return Promise.all([ return Promise.all([
db.products.insertMany(jsonData.products), db.products.insertMany(jsonData.products),
db.menu.insertOne(jsonData.menu), db.menu.insertOne(jsonData.menu)
exports.runIndexing(app) ]);
});
};
exports.testData = async (app) => {
const db = app.db;
const testData = fs.readFileSync('./bin/testdata.json', 'utf-8');
const jsonData = JSON.parse(testData);
// TODO: A bit ugly, needs fixing
return new Promise((resolve, reject) => {
Promise.all([
db.users.remove({}, {}),
db.customers.remove({}, {}),
db.products.remove({}, {})
]) ])
.then(() => {
Promise.all([
db.users.insertMany(jsonData.users),
db.customers.insertMany(jsonData.customers),
db.products.insertMany(jsonData.products),
db.menu.insertOne(jsonData.menu)
])
.then(() => {
resolve();
})
.catch((err) => { .catch((err) => {
console.info(colors.red('Error inserting test data. Check `/bin/testdata.json` is correctly formatted.', err)); reject(err);
process.exit(2); });
})
.catch((err) => {
reject(err);
}); });
}); });
}; };

3851
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"private": false, "private": false,
"scripts": { "scripts": {
"start": "node app.js", "start": "node app.js",
"deploy": "gulp deploy" "deploy": "gulp deploy",
"test": "ava test/**/*.js --verbose"
}, },
"dependencies": { "dependencies": {
"ajv": "^6.0.0", "ajv": "^6.0.0",
@ -16,7 +17,7 @@
"body-parser": "^1.17.2", "body-parser": "^1.17.2",
"cheerio": "^0.22.0", "cheerio": "^0.22.0",
"colors": "^1.1.2", "colors": "^1.1.2",
"connect-mongodb-session": "^1.3.0", "connect-mongodb-session": "^1.4.0",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"express": "^4.15.3", "express": "^4.15.3",
"express-handlebars": "^3.0.0", "express-handlebars": "^3.0.0",
@ -27,8 +28,7 @@
"lodash": "^4.13.1", "lodash": "^4.13.1",
"lunr": "^2.1.5", "lunr": "^2.1.5",
"moment": "^2.15.2", "moment": "^2.15.2",
"mongodb": "^3.0.1", "mongodb": "2.2.3",
"mongodb-uri": "^0.9.7",
"morgan": "^1.9.0", "morgan": "^1.9.0",
"multer": "^1.1.0", "multer": "^1.1.0",
"nodemailer": "^4.4.1", "nodemailer": "^4.4.1",
@ -42,6 +42,7 @@
"uglifycss": "0.0.27" "uglifycss": "0.0.27"
}, },
"devDependencies": { "devDependencies": {
"ava": "^0.25.0",
"eslint": "^3.19.0", "eslint": "^3.19.0",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^12.1.0",
"eslint-config-standard": "^10.2.1", "eslint-config-standard": "^10.2.1",
@ -53,7 +54,9 @@
"gulp-clean-css": "^3.9.2", "gulp-clean-css": "^3.9.2",
"gulp-minify": "^2.1.0", "gulp-minify": "^2.1.0",
"gulp-rename": "^1.2.2", "gulp-rename": "^1.2.2",
"run-sequence": "^2.2.1" "mongodb-uri": "^0.9.7",
"run-sequence": "^2.2.1",
"wait.for": "^0.6.6"
}, },
"main": "app.js", "main": "app.js",
"keywords": [ "keywords": [

View File

@ -344,6 +344,27 @@ $(document).ready(function (){
} }
}); });
$('#loginForm').on('click', function(e){
if(!e.isDefaultPrevented()){
e.preventDefault();
$.ajax({
method: 'POST',
url: '/admin/login_action',
data: {
email: $('#email').val(),
password: $('#password').val()
}
})
.done(function(msg){
window.location = '/admin';
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
}
e.preventDefault();
});
// call update settings API // call update settings API
$('#customerLogin').on('click', function(e){ $('#customerLogin').on('click', function(e){
if(!e.isDefaultPrevented()){ if(!e.isDefaultPrevented()){

File diff suppressed because one or more lines are too long

View File

@ -39,7 +39,7 @@ router.get('/admin/login', (req, res) => {
res.render('login', { res.render('login', {
title: 'Login', title: 'Login',
referringUrl: req.header('Referer'), referringUrl: req.header('Referer'),
config: common.getConfig(), config: req.app.config,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
@ -59,17 +59,13 @@ router.post('/admin/login_action', (req, res) => {
db.users.findOne({userEmail: req.body.email}, (err, user) => { db.users.findOne({userEmail: req.body.email}, (err, user) => {
if(err){ if(err){
req.session.message = 'Cannot find user.'; res.status(400).json({message: 'A user with that email does not exist.'});
req.session.messageType = 'danger';
res.redirect('/admin/login');
return; return;
} }
// check if user exists with that email // check if user exists with that email
if(user === undefined || user === null){ if(user === undefined || user === null){
req.session.message = 'A user with that email does not exist.'; res.status(400).json({message: 'A user with that email does not exist.'});
req.session.messageType = 'danger';
res.redirect('/admin/login');
}else{ }else{
// we have a user under that email so we compare the password // we have a user under that email so we compare the password
bcrypt.compare(req.body.password, user.userPassword) bcrypt.compare(req.body.password, user.userPassword)
@ -79,12 +75,10 @@ router.post('/admin/login_action', (req, res) => {
req.session.usersName = user.usersName; req.session.usersName = user.usersName;
req.session.userId = user._id.toString(); req.session.userId = user._id.toString();
req.session.isAdmin = user.isAdmin; req.session.isAdmin = user.isAdmin;
res.redirect('/admin'); res.status(200).json({message: 'Login successful'});
}else{ }else{
// password is not correct // password is not correct
req.session.message = 'Access denied. Check password and try again.'; res.status(400).json({message: 'Access denied. Check password and try again.'});
req.session.messageType = 'danger';
res.redirect('/admin/login');
} }
}); });
} }
@ -106,7 +100,7 @@ router.get('/admin/setup', (req, res) => {
req.session.needsSetup = true; req.session.needsSetup = true;
res.render('setup', { res.render('setup', {
title: 'Setup', title: 'Setup',
config: common.getConfig(), config: req.app.config,
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
@ -165,9 +159,9 @@ router.get('/admin/settings', common.restrict, (req, res) => {
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig(), config: req.app.config,
footerHtml: typeof common.getConfig().footerHtml !== 'undefined' ? escape.decode(common.getConfig().footerHtml) : null, footerHtml: typeof req.app.config.footerHtml !== 'undefined' ? escape.decode(req.app.config.footerHtml) : null,
googleAnalytics: typeof common.getConfig().googleAnalytics !== 'undefined' ? escape.decode(common.getConfig().googleAnalytics) : null googleAnalytics: typeof req.app.config.googleAnalytics !== 'undefined' ? escape.decode(req.app.config.googleAnalytics) : null
}); });
}); });
@ -218,7 +212,7 @@ router.get('/admin/settings/menu', common.restrict, async (req, res) => {
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig(), config: req.app.config,
menu: common.sortMenu(await common.getMenu(db)) menu: common.sortMenu(await common.getMenu(db))
}); });
}); });
@ -239,7 +233,7 @@ router.get('/admin/settings/pages', common.restrict, (req, res) => {
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig(), config: req.app.config,
menu: common.sortMenu(await common.getMenu(db)) menu: common.sortMenu(await common.getMenu(db))
}); });
}); });
@ -257,7 +251,7 @@ router.get('/admin/settings/pages/new', common.restrict, common.checkAccess, asy
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig(), config: req.app.config,
menu: common.sortMenu(await common.getMenu(db)) menu: common.sortMenu(await common.getMenu(db))
}); });
}); });
@ -281,14 +275,14 @@ router.get('/admin/settings/pages/edit/:page', common.restrict, common.checkAcce
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig(), config: req.app.config,
menu menu
}); });
}else{ }else{
// 404 it! // 404 it!
res.status(404).render('error', { res.status(404).render('error', {
title: '404 Error - Page not found', title: '404 Error - Page not found',
config: common.getConfig(), config: req.app.config,
message: '404 Error - Page not found', message: '404 Error - Page not found',
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
showFooter: 'showFooter', showFooter: 'showFooter',
@ -478,7 +472,7 @@ router.post('/admin/file/upload', common.restrict, common.checkAccess, upload.si
// delete a file via ajax request // delete a file via ajax request
router.post('/admin/testEmail', common.restrict, (req, res) => { router.post('/admin/testEmail', common.restrict, (req, res) => {
let config = common.getConfig(); let config = req.app.config;
// TODO: Should fix this to properly handle result // TODO: Should fix this to properly handle result
common.sendEmail(config.emailAddress, 'expressCart test email', 'Your email settings are working'); common.sendEmail(config.emailAddress, 'expressCart test email', 'Your email settings are working');
res.status(200).json({message: 'Test email sent'}); res.status(200).json({message: 'Test email sent'});

View File

@ -74,7 +74,7 @@ router.get('/admin/customer/view/:id?', common.restrict, (req, res) => {
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
config: common.getConfig(), config: req.app.config,
editor: true, editor: true,
helpers: req.handlebars.helpers helpers: req.handlebars.helpers
}); });
@ -94,7 +94,7 @@ router.get('/admin/customers', common.restrict, (req, res) => {
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
config: common.getConfig() config: req.app.config
}); });
}); });
}); });
@ -119,7 +119,7 @@ router.get('/admin/customers/filter/:search', common.restrict, (req, res, next)
title: 'Customer results', title: 'Customer results',
customers: customers, customers: customers,
admin: true, admin: true,
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
searchTerm: searchTerm, searchTerm: searchTerm,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
@ -130,7 +130,7 @@ router.get('/admin/customers/filter/:search', common.restrict, (req, res, next)
}); });
// login the customer and check the password // login the customer and check the password
router.post('/customer/login_action', (req, res) => { router.post('/customer/login_action', async (req, res) => {
let db = req.app.db; let db = req.app.db;
db.customers.findOne({email: req.body.loginEmail}, (err, customer) => { // eslint-disable-line db.customers.findOne({email: req.body.loginEmail}, (err, customer) => { // eslint-disable-line
@ -178,7 +178,7 @@ router.get('/customer/forgotten', (req, res) => {
title: 'Forgotten', title: 'Forgotten',
route: 'customer', route: 'customer',
forgotType: 'customer', forgotType: 'customer',
config: common.getConfig(), config: req.app.config,
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
@ -189,7 +189,7 @@ router.get('/customer/forgotten', (req, res) => {
// forgotten password // forgotten password
router.post('/customer/forgotten_action', (req, res) => { router.post('/customer/forgotten_action', (req, res) => {
const db = req.app.db; const db = req.app.db;
const config = common.getConfig(); const config = req.app.config;
let passwordToken = randtoken.generate(30); let passwordToken = randtoken.generate(30);
// find the user // find the user
@ -240,7 +240,7 @@ router.get('/customer/reset/:token', (req, res) => {
title: 'Reset password', title: 'Reset password',
token: req.params.token, token: req.params.token,
route: 'customer', route: 'customer',
config: common.getConfig(), config: req.app.config,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
message_type: common.clearSessionValue(req.session, 'message_type'), message_type: common.clearSessionValue(req.session, 'message_type'),
show_footer: 'show_footer', show_footer: 'show_footer',

View File

@ -6,10 +6,9 @@ const _ = require('lodash');
const common = require('../lib/common'); const common = require('../lib/common');
// These is the customer facing routes // These is the customer facing routes
router.get('/payment/:orderId', async (req, res, next) => { router.get('/payment/:orderId', async (req, res, next) => {
let db = req.app.db; let db = req.app.db;
let config = common.getConfig(); let config = req.app.config;
// render the payment complete message // render the payment complete message
db.orders.findOne({_id: common.getId(req.params.orderId)}, async (err, result) => { db.orders.findOne({_id: common.getId(req.params.orderId)}, async (err, result) => {
@ -18,7 +17,7 @@ router.get('/payment/:orderId', async (req, res, next) => {
} }
res.render(`${config.themeViews}payment_complete`, { res.render(`${config.themeViews}payment_complete`, {
title: 'Payment complete', title: 'Payment complete',
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
pageCloseBtn: common.showCartCloseBtn('payment'), pageCloseBtn: common.showCartCloseBtn('payment'),
result: result, result: result,
@ -32,7 +31,7 @@ router.get('/payment/:orderId', async (req, res, next) => {
}); });
router.get('/checkout', async (req, res, next) => { router.get('/checkout', async (req, res, next) => {
let config = common.getConfig(); let config = req.app.config;
// if there is no items in the cart then render a failure // if there is no items in the cart then render a failure
if(!req.session.cart){ if(!req.session.cart){
@ -45,7 +44,7 @@ router.get('/checkout', async (req, res, next) => {
// render the checkout // render the checkout
res.render(`${config.themeViews}checkout`, { res.render(`${config.themeViews}checkout`, {
title: 'Checkout', title: 'Checkout',
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
pageCloseBtn: common.showCartCloseBtn('checkout'), pageCloseBtn: common.showCartCloseBtn('checkout'),
checkout: 'hidden', checkout: 'hidden',
@ -58,7 +57,7 @@ router.get('/checkout', async (req, res, next) => {
}); });
router.get('/pay', async (req, res, next) => { router.get('/pay', async (req, res, next) => {
const config = common.getConfig(); const config = req.app.config;
// if there is no items in the cart then render a failure // if there is no items in the cart then render a failure
if(!req.session.cart){ if(!req.session.cart){
@ -71,7 +70,7 @@ router.get('/pay', async (req, res, next) => {
// render the payment page // render the payment page
res.render(`${config.themeViews}pay`, { res.render(`${config.themeViews}pay`, {
title: 'Pay', title: 'Pay',
config: common.getConfig(), config: req.app.config,
paymentConfig: common.getPaymentConfig(), paymentConfig: common.getPaymentConfig(),
pageCloseBtn: common.showCartCloseBtn('pay'), pageCloseBtn: common.showCartCloseBtn('pay'),
session: req.session, session: req.session,
@ -85,14 +84,14 @@ router.get('/pay', async (req, res, next) => {
}); });
router.get('/cartPartial', (req, res) => { router.get('/cartPartial', (req, res) => {
const config = common.getConfig(); const config = req.app.config;
res.render(`${config.themeViews}cart`, { res.render(`${config.themeViews}cart`, {
pageCloseBtn: common.showCartCloseBtn(req.query.path), pageCloseBtn: common.showCartCloseBtn(req.query.path),
page: req.query.path, page: req.query.path,
layout: false, layout: false,
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig(), config: req.app.config,
session: req.session session: req.session
}); });
}); });
@ -100,7 +99,7 @@ router.get('/cartPartial', (req, res) => {
// show an individual product // show an individual product
router.get('/product/:id', (req, res) => { router.get('/product/:id', (req, res) => {
let db = req.app.db; let db = req.app.db;
let config = common.getConfig(); let config = req.app.config;
db.products.findOne({$or: [{_id: common.getId(req.params.id)}, {productPermalink: req.params.id}]}, (err, result) => { db.products.findOne({$or: [{_id: common.getId(req.params.id)}, {productPermalink: req.params.id}]}, (err, result) => {
// render 404 if page is not published // render 404 if page is not published
@ -115,6 +114,12 @@ router.get('/product/:id', (req, res) => {
productOptions = JSON.parse(result.productOptions); productOptions = JSON.parse(result.productOptions);
} }
// If JSON query param return json instead
if(req.query.json === 'true'){
res.status(200).json(result);
return;
}
// show the view // show the view
common.getImages(result._id, req, res, async (images) => { common.getImages(result._id, req, res, async (images) => {
res.render(`${config.themeViews}product`, { res.render(`${config.themeViews}product`, {
@ -223,10 +228,14 @@ router.post('/product/addtocart', (req, res, next) => {
db.products.findOne({_id: common.getId(req.body.productId)}, (err, product) => { db.products.findOne({_id: common.getId(req.body.productId)}, (err, product) => {
if(err){ if(err){
console.error(colors.red('Error adding to cart', err)); console.error(colors.red('Error adding to cart', err));
return res.status(400).json({message: 'Error updating cart. Please try again.'});
}
// No product found
if(!product){
return res.status(400).json({message: 'Error updating cart. Please try again.'});
} }
// We item is found, add it to the cart
if(product){
let productPrice = parseFloat(product.productPrice).toFixed(2); let productPrice = parseFloat(product.productPrice).toFixed(2);
// Doc used to test if existing in the cart with the options. If not found, we add new. // Doc used to test if existing in the cart with the options. If not found, we add new.
@ -271,10 +280,7 @@ router.post('/product/addtocart', (req, res, next) => {
// update how many products in the shopping cart // update how many products in the shopping cart
req.session.cartTotalItems = Object.keys(req.session.cart).length; req.session.cartTotalItems = Object.keys(req.session.cart).length;
res.status(200).json({message: 'Cart successfully updated', totalCartItems: Object.keys(req.session.cart).length}); return res.status(200).json({message: 'Cart successfully updated', totalCartItems: Object.keys(req.session.cart).length});
}else{
res.status(400).json({message: 'Error updating cart. Please try again.'});
}
}); });
}); });
@ -283,7 +289,7 @@ router.get('/search/:searchTerm/:pageNum?', (req, res) => {
let db = req.app.db; let db = req.app.db;
let searchTerm = req.params.searchTerm; let searchTerm = req.params.searchTerm;
let productsIndex = req.app.productsIndex; let productsIndex = req.app.productsIndex;
let config = common.getConfig(); let config = req.app.config;
let numberProducts = config.productsPerPage ? config.productsPerPage : 6; let numberProducts = config.productsPerPage ? config.productsPerPage : 6;
let lunrIdArray = []; let lunrIdArray = [];
@ -301,12 +307,18 @@ router.get('/search/:searchTerm/:pageNum?', (req, res) => {
common.getMenu(db) common.getMenu(db)
]) ])
.then(([results, menu]) => { .then(([results, menu]) => {
// If JSON query param return json instead
if(req.query.json === 'true'){
res.status(200).json(results.data);
return;
}
res.render(`${config.themeViews}index`, { res.render(`${config.themeViews}index`, {
title: 'Results', title: 'Results',
results: results.data, results: results.data,
filtered: true, filtered: true,
session: req.session, session: req.session,
metaDescription: common.getConfig().cartTitle + ' - Search term: ' + searchTerm, metaDescription: req.app.config.cartTitle + ' - Search term: ' + searchTerm,
searchTerm: searchTerm, searchTerm: searchTerm,
pageCloseBtn: common.showCartCloseBtn('search'), pageCloseBtn: common.showCartCloseBtn('search'),
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
@ -331,7 +343,7 @@ router.get('/category/:cat/:pageNum?', (req, res) => {
let db = req.app.db; let db = req.app.db;
let searchTerm = req.params.cat; let searchTerm = req.params.cat;
let productsIndex = req.app.productsIndex; let productsIndex = req.app.productsIndex;
let config = common.getConfig(); let config = req.app.config;
let numberProducts = config.productsPerPage ? config.productsPerPage : 6; let numberProducts = config.productsPerPage ? config.productsPerPage : 6;
let lunrIdArray = []; let lunrIdArray = [];
@ -351,13 +363,19 @@ router.get('/category/:cat/:pageNum?', (req, res) => {
.then(([results, menu]) => { .then(([results, menu]) => {
const sortedMenu = common.sortMenu(menu); const sortedMenu = common.sortMenu(menu);
// If JSON query param return json instead
if(req.query.json === 'true'){
res.status(200).json(results.data);
return;
}
res.render(`${config.themeViews}index`, { res.render(`${config.themeViews}index`, {
title: 'Category', title: 'Category',
results: results.data, results: results.data,
filtered: true, filtered: true,
session: req.session, session: req.session,
searchTerm: searchTerm, searchTerm: searchTerm,
metaDescription: common.getConfig().cartTitle + ' - Category: ' + searchTerm, metaDescription: req.app.config.cartTitle + ' - Category: ' + searchTerm,
pageCloseBtn: common.showCartCloseBtn('category'), pageCloseBtn: common.showCartCloseBtn('category'),
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
@ -380,7 +398,7 @@ router.get('/category/:cat/:pageNum?', (req, res) => {
// return sitemap // return sitemap
router.get('/sitemap.xml', (req, res, next) => { router.get('/sitemap.xml', (req, res, next) => {
let sm = require('sitemap'); let sm = require('sitemap');
let config = common.getConfig(); let config = req.app.config;
common.addSitemapProducts(req, res, (err, products) => { common.addSitemapProducts(req, res, (err, products) => {
if(err){ if(err){
@ -412,7 +430,7 @@ router.get('/sitemap.xml', (req, res, next) => {
router.get('/page/:pageNum', (req, res, next) => { router.get('/page/:pageNum', (req, res, next) => {
let db = req.app.db; let db = req.app.db;
let config = common.getConfig(); let config = req.app.config;
let numberProducts = config.productsPerPage ? config.productsPerPage : 6; let numberProducts = config.productsPerPage ? config.productsPerPage : 6;
Promise.all([ Promise.all([
@ -420,15 +438,21 @@ router.get('/page/:pageNum', (req, res, next) => {
common.getMenu(db) common.getMenu(db)
]) ])
.then(([results, menu]) => { .then(([results, menu]) => {
// If JSON query param return json instead
if(req.query.json === 'true'){
res.status(200).json(results.data);
return;
}
res.render(`${config.themeViews}index`, { res.render(`${config.themeViews}index`, {
title: 'Shop', title: 'Shop',
results: results.data, results: results.data,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
metaDescription: common.getConfig().cartTitle + ' - Products page: ' + req.params.pageNum, metaDescription: req.app.config.cartTitle + ' - Products page: ' + req.params.pageNum,
pageCloseBtn: common.showCartCloseBtn('page'), pageCloseBtn: common.showCartCloseBtn('page'),
config: common.getConfig(), config: req.app.config,
productsPerPage: numberProducts, productsPerPage: numberProducts,
totalProductCount: results.totalProducts, totalProductCount: results.totalProducts,
pageNum: req.params.pageNum, pageNum: req.params.pageNum,
@ -446,7 +470,7 @@ router.get('/page/:pageNum', (req, res, next) => {
// The main entry point of the shop // The main entry point of the shop
router.get('/:page?', (req, res, next) => { router.get('/:page?', (req, res, next) => {
let db = req.app.db; let db = req.app.db;
let config = common.getConfig(); let config = req.app.config;
let numberProducts = config.productsPerPage ? config.productsPerPage : 6; let numberProducts = config.productsPerPage ? config.productsPerPage : 6;
// if no page is specified, just render page 1 of the cart // if no page is specified, just render page 1 of the cart
@ -456,6 +480,12 @@ router.get('/:page?', (req, res, next) => {
common.getMenu(db) common.getMenu(db)
]) ])
.then(([results, menu]) => { .then(([results, menu]) => {
// If JSON query param return json instead
if(req.query.json === 'true'){
res.status(200).json(results.data);
return;
}
res.render(`${config.themeViews}index`, { res.render(`${config.themeViews}index`, {
title: `${config.cartTitle} - Shop`, title: `${config.cartTitle} - Shop`,
theme: config.theme, theme: config.theme,
@ -464,7 +494,7 @@ router.get('/:page?', (req, res, next) => {
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
pageCloseBtn: common.showCartCloseBtn('page'), pageCloseBtn: common.showCartCloseBtn('page'),
config: common.getConfig(), config: req.app.config,
productsPerPage: numberProducts, productsPerPage: numberProducts,
totalProductCount: results.totalProducts, totalProductCount: results.totalProducts,
pageNum: 1, pageNum: 1,
@ -497,8 +527,8 @@ router.get('/:page?', (req, res, next) => {
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
pageCloseBtn: common.showCartCloseBtn('page'), pageCloseBtn: common.showCartCloseBtn('page'),
config: common.getConfig(), config: req.app.config,
metaDescription: common.getConfig().cartTitle + ' - ' + page, metaDescription: req.app.config.cartTitle + ' - ' + page,
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
showFooter: 'showFooter', showFooter: 'showFooter',
menu: common.sortMenu(await common.getMenu(db)) menu: common.sortMenu(await common.getMenu(db))
@ -506,7 +536,7 @@ router.get('/:page?', (req, res, next) => {
}else{ }else{
res.status(404).render('error', { res.status(404).render('error', {
title: '404 Error - Page not found', title: '404 Error - Page not found',
config: common.getConfig(), config: req.app.config,
message: '404 Error - Page not found', message: '404 Error - Page not found',
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
showFooter: 'showFooter', showFooter: 'showFooter',

View File

@ -15,7 +15,7 @@ router.get('/admin/orders', common.restrict, (req, res, next) => {
title: 'Cart', title: 'Cart',
orders: orders, orders: orders,
admin: true, admin: true,
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
@ -45,7 +45,7 @@ router.get('/admin/orders/bystatus/:orderstatus', common.restrict, (req, res, ne
admin: true, admin: true,
filteredOrders: true, filteredOrders: true,
filteredStatus: req.params.orderstatus, filteredStatus: req.params.orderstatus,
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
@ -69,7 +69,7 @@ router.get('/admin/order/view/:id', common.restrict, (req, res) => {
title: 'View order', title: 'View order',
result: result, result: result,
productOptions: productOptions, productOptions: productOptions,
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
@ -100,7 +100,7 @@ router.get('/admin/orders/filter/:search', common.restrict, (req, res, next) =>
title: 'Order results', title: 'Order results',
orders: orders, orders: orders,
admin: true, admin: true,
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
searchTerm: searchTerm, searchTerm: searchTerm,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),

View File

@ -7,7 +7,7 @@ const router = express.Router();
// The homepage of the site // The homepage of the site
router.post('/checkout_action', (req, res, next) => { router.post('/checkout_action', (req, res, next) => {
const db = req.app.db; const db = req.app.db;
const config = common.getConfig(); const config = req.app.config;
const authorizenetConfig = common.getPaymentConfig(); const authorizenetConfig = common.getPaymentConfig();
let authorizeUrl = 'https://api.authorize.net/xml/v1/request.api'; let authorizeUrl = 'https://api.authorize.net/xml/v1/request.api';

View File

@ -10,7 +10,7 @@ router.get('/checkout_cancel', (req, res, next) => {
router.get('/checkout_return', (req, res, next) => { router.get('/checkout_return', (req, res, next) => {
let db = req.app.db; let db = req.app.db;
let config = common.getConfig(); let config = req.app.config;
let paymentId = req.session.paymentId; let paymentId = req.session.paymentId;
let payerId = req.query['PayerID']; let payerId = req.query['PayerID'];
@ -107,7 +107,7 @@ router.get('/checkout_return', (req, res, next) => {
// The homepage of the site // The homepage of the site
router.post('/checkout_action', (req, res, next) => { router.post('/checkout_action', (req, res, next) => {
let db = req.app.db; let db = req.app.db;
let config = common.getConfig(); let config = req.app.config;
let paypalConfig = common.getPaymentConfig(); let paypalConfig = common.getPaymentConfig();
// setup the payment object // setup the payment object

View File

@ -7,7 +7,7 @@ let router = express.Router();
// The homepage of the site // The homepage of the site
router.post('/checkout_action', (req, res, next) => { router.post('/checkout_action', (req, res, next) => {
let db = req.app.db; let db = req.app.db;
let config = common.getConfig(); let config = req.app.config;
let stripeConfig = common.getPaymentConfig(); let stripeConfig = common.getPaymentConfig();
// charge via stripe // charge via stripe

View File

@ -18,7 +18,7 @@ router.get('/admin/products', common.restrict, (req, res, next) => {
top_results: topResults, top_results: topResults,
session: req.session, session: req.session,
admin: true, admin: true,
config: common.getConfig(), config: req.app.config,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers helpers: req.handlebars.helpers
@ -45,7 +45,7 @@ router.get('/admin/products/filter/:search', (req, res, next) => {
title: 'Results', title: 'Results',
results: results, results: results,
admin: true, admin: true,
config: common.getConfig(), config: req.app.config,
session: req.session, session: req.session,
searchTerm: searchTerm, searchTerm: searchTerm,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
@ -69,7 +69,7 @@ router.get('/admin/product/new', common.restrict, common.checkAccess, (req, res)
editor: true, editor: true,
admin: true, admin: true,
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig() config: req.app.config
}); });
}); });
@ -167,7 +167,7 @@ router.get('/admin/product/edit/:id', common.restrict, common.checkAccess, (req,
session: req.session, session: req.session,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
config: common.getConfig(), config: req.app.config,
editor: true, editor: true,
helpers: req.handlebars.helpers helpers: req.handlebars.helpers
}); });

View File

@ -15,7 +15,7 @@ router.get('/admin/users', common.restrict, (req, res) => {
title: 'Users', title: 'Users',
users: users, users: users,
admin: true, admin: true,
config: common.getConfig(), config: req.app.config,
isAdmin: req.session.isAdmin, isAdmin: req.session.isAdmin,
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
session: req.session, session: req.session,
@ -49,7 +49,7 @@ router.get('/admin/user/edit/:id', common.restrict, (req, res) => {
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
config: common.getConfig() config: req.app.config
}); });
}); });
}); });
@ -63,7 +63,7 @@ router.get('/admin/user/new', common.restrict, (req, res) => {
helpers: req.handlebars.helpers, helpers: req.handlebars.helpers,
message: common.clearSessionValue(req.session, 'message'), message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'), messageType: common.clearSessionValue(req.session, 'messageType'),
config: common.getConfig() config: req.app.config
}); });
}); });

147
test/test.js Normal file
View File

@ -0,0 +1,147 @@
const test = require('ava');
const axios = require('axios');
const fs = require('fs');
const app = require('../app');
const common = require('../lib/common');
// Get test data to compare in tests
const rawTestData = fs.readFileSync('./bin/testdata.json', 'utf-8');
const testData = JSON.parse(rawTestData);
let config;
let db;
let baseUrl;
let products;
let customers;
let users;
// Start up app and wait for it to be ready
test.before.cb(t => {
app.on('appStarted', async () => {
// Set some stuff now we have the app started
config = app.config;
db = app.db;
baseUrl = `http://localhost:${app.port}`;
await common.testData(app);
products = await db.products.find({}).toArray();
customers = await db.customers.find({}).toArray();
users = await db.users.find({}).toArray();
t.end();
});
});
test('[Success] Get products JSON', t => {
return new Promise((resolve, reject) => {
axios.get(`${baseUrl}?json=true`)
.then((response) => {
if(response.data.length < config.productsPerPage){
t.is(response.data.length, testData.products.length);
}else{
t.is(response.data.length, config.productsPerPage);
}
t.pass();
resolve();
})
.catch((error) => {
reject(new Error('Should not be allowed'));
});
});
});
test('[Success] User Login', t => {
return new Promise((resolve, reject) => {
axios.post(`${baseUrl}/admin/login_action`, {
email: users[0].userEmail,
password: 'test'
})
.then((response) => {
t.deepEqual(response.data.message, 'Login successful');
resolve();
})
.catch((error) => {
reject(new Error('Should not be allowed'));
});
});
});
test('[Fail] Incorrect user password', t => {
return new Promise((resolve, reject) => {
axios.post(`${baseUrl}/admin/login_action`, {
email: users[0].userEmail,
password: 'test1'
})
.then((response) => {
reject(new Error('Should not be allowed'));
})
.catch((error) => {
t.deepEqual(error.response.data.message, 'Access denied. Check password and try again.');
resolve();
});
});
});
test('[Fail] Customer login with incorrect email', t => {
return new Promise((resolve, reject) => {
axios.post(`${baseUrl}/customer/login_action`, {
loginEmail: 'test1@test.com',
loginPassword: 'test'
})
.then((response) => {
reject(new Error('Should not be allowed'));
})
.catch((error) => {
t.deepEqual(error.response.data.err, 'A customer with that email does not exist.');
resolve();
});
});
});
test('[Success] Customer login with correct email', t => {
return new Promise((resolve, reject) => {
axios.post(`${baseUrl}/customer/login_action`, {
loginEmail: 'test@test.com',
loginPassword: 'test'
})
.then((response) => {
t.deepEqual(response.data.message, 'Successfully logged in');
resolve();
})
.catch((error) => {
reject(new Error('Should not be allowed'));
});
});
});
test('[Success] Add product to cart', t => {
return new Promise((resolve, reject) => {
axios.post(`${baseUrl}/product/addtocart`, {
productId: products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(products[0].productOptions)
})
.then((response) => {
t.deepEqual(response.data.message, 'Cart successfully updated');
resolve();
})
.catch((error) => {
reject(new Error('Should not be allowed'));
});
});
});
test('[Fail] Add incorrect product to cart', t => {
return new Promise((resolve, reject) => {
axios.post(`${baseUrl}/product/addtocart`, {
productId: 'someid'
})
.then((response) => {
t.deepEqual(response.data.message, 'Successfully logged in');
reject(new Error('Should not be allowed'));
resolve();
})
.catch((error) => {
t.deepEqual(error.response.data.message, 'Error updating cart. Please try again.');
resolve();
});
});
});

View File

@ -1,13 +1,13 @@
<div class="col-md-offset-4 col-md-4 col-lg-offset-4 col-lg-4" style="padding-top: 100px" > <div class="col-md-offset-4 col-md-4 col-lg-offset-4 col-lg-4" style="padding-top: 100px" >
<form class="form-signin" action="login_action" method="post" role="form" data-toggle="validator"> <form class="form-signin" method="post" role="form" data-toggle="validator">
<input type="hidden" name="frm_referringUrl" value="{{referringUrl}}"> <input type="hidden" name="frm_referringUrl" value="{{referringUrl}}">
<h2 class="form-signin-heading">Please sign in</h2> <h2 class="form-signin-heading">Please sign in</h2>
<div class="form-group"> <div class="form-group">
<input type="email" name="email" class="form-control" placeholder="email address" required autofocus> <input type="email" id="email" name="email" class="form-control" placeholder="email address" required autofocus>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" name="password" class="form-control" placeholder="Password" required> <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</div> </div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> <button class="btn btn-lg btn-primary btn-block" id="loginForm" type="submit">Sign in</button>
</form> </form>
</div> </div>