expressCart/app.js

500 lines
15 KiB
JavaScript

const fs = require('fs');
const yenv = require('yenv');
if(fs.existsSync('./env.yaml')){
process.env = yenv('env.yaml', { strict: false });
}
const path = require('path');
const express = require('express');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const session = require('express-session');
const moment = require('moment');
const _ = require('lodash');
const MongoStore = require('connect-mongodb-session')(session);
const numeral = require('numeral');
const helmet = require('helmet');
const colors = require('colors');
const cron = require('node-cron');
const crypto = require('crypto');
const common = require('./lib/common');
const { runIndexing } = require('./lib/indexing');
const { addSchemas } = require('./lib/schema');
const { initDb, getDbUri } = require('./lib/db');
let handlebars = require('express-handlebars');
const i18n = require('i18n');
// Validate our settings schema
const Ajv = require('ajv');
const ajv = new Ajv({ useDefaults: true });
// get config
const config = common.getConfig();
const baseConfig = ajv.validate(require('./config/settingsSchema'), config);
if(baseConfig === false){
console.log(colors.red(`settings.json incorrect: ${ajv.errorsText()}`));
process.exit(2);
}
// Validate the payment gateway config
if(ajv.validate(
require(`./config/payment/schema/${config.paymentGateway}`),
require(`./config/payment/config/${config.paymentGateway}`)) === false
){
console.log(colors.red(`${config.paymentGateway} config is incorrect: ${ajv.errorsText()}`));
process.exit(2);
}
// require the routes
const index = require('./routes/index');
const admin = require('./routes/admin');
const product = require('./routes/product');
const customer = require('./routes/customer');
const order = require('./routes/order');
const user = require('./routes/user');
// Add the payment route
const paymentRoute = require(`./lib/payments/${config.paymentGateway}`);
const app = express();
// Language initialize
i18n.configure({
locales: config.availableLanguages,
defaultLocale: config.defaultLocale,
cookie: 'locale',
queryParameter: 'lang',
directory: `${__dirname}/locales`,
directoryPermissions: '755',
api: {
__: '__', // now req.__ becomes req.__
__n: '__n' // and req.__n can be called as req.__n
}
});
// view engine setup
app.set('views', path.join(__dirname, '/views'));
app.engine('hbs', handlebars({
extname: 'hbs',
layoutsDir: path.join(__dirname, 'views', 'layouts'),
defaultLayout: 'layout.hbs',
partialsDir: [path.join(__dirname, 'views')]
}));
app.set('view engine', 'hbs');
// helpers for the handlebar templating platform
handlebars = handlebars.create({
helpers: {
// Language helper
__: () => { return i18n.__(this, arguments); }, // eslint-disable-line no-undef
__n: () => { return i18n.__n(this, arguments); }, // eslint-disable-line no-undef
availableLanguages: (block) => {
let total = '';
for(const lang of i18n.getLocales()){
total += block.fn(lang);
}
return total;
},
partial: (provider) => {
return `partials/payments/${provider}`;
},
perRowClass: (numProducts) => {
if(parseInt(numProducts) === 1){
return 'col-6 col-md-12 product-item';
}
if(parseInt(numProducts) === 2){
return 'col-6 col-md-6 product-item';
}
if(parseInt(numProducts) === 3){
return 'col-6 col-md-4 product-item';
}
if(parseInt(numProducts) === 4){
return 'col-6 col-md-3 product-item';
}
return 'col-md-6 product-item';
},
menuMatch: (title, search) => {
if(!title || !search){
return '';
}
if(title.toLowerCase().startsWith(search.toLowerCase())){
return 'class="navActive"';
}
return '';
},
getTheme: (view) => {
return `themes/${config.theme}/${view}`;
},
formatAmount: (amt) => {
if(amt){
return numeral(amt).format('0.00');
}
return '0.00';
},
amountNoDecimal: (amt) => {
if(amt){
return handlebars.helpers.formatAmount(amt).replace('.', '');
}
return handlebars.helpers.formatAmount(amt);
},
getStatusColor: (status) => {
switch(status){
case 'Paid':
return 'success';
case 'Approved':
return 'success';
case 'Approved - Processing':
return 'success';
case 'Failed':
return 'danger';
case 'Completed':
return 'success';
case 'Shipped':
return 'success';
case 'Pending':
return 'warning';
default:
return 'danger';
}
},
checkProductOptions: (opts) => {
if(opts){
return 'true';
}
return 'false';
},
currencySymbol: (value) => {
if(typeof value === 'undefined' || value === ''){
return '$';
}
return value;
},
objectLength: (obj) => {
if(obj){
return Object.keys(obj).length;
}
return 0;
},
stringify: (obj) => {
if(obj){
return JSON.stringify(obj);
}
return '';
},
checkedState: (state) => {
if(state === 'true' || state === true){
return 'checked';
}
return '';
},
selectState: (state, value) => {
if(state === value){
return 'selected';
}
return '';
},
isNull: (value, options) => {
if(typeof value === 'undefined' || value === ''){
return options.fn(this);
}
return options.inverse(this);
},
toLower: (value) => {
if(value){
return value.toLowerCase();
}
return null;
},
formatDate: (date, format) => {
return moment(date).format(format);
},
discountExpiry: (start, end) => {
return moment().isBetween(moment(start), moment(end));
},
ifCond: (v1, operator, v2, options) => {
switch(operator){
case '==':
return (v1 === v2) ? options.fn(this) : options.inverse(this);
case '!=':
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
case '===':
return (v1 === v2) ? options.fn(this) : options.inverse(this);
case '<':
return (v1 < v2) ? options.fn(this) : options.inverse(this);
case '<=':
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
case '>':
return (v1 > v2) ? options.fn(this) : options.inverse(this);
case '>=':
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
case '&&':
return (v1 && v2) ? options.fn(this) : options.inverse(this);
case '||':
return (v1 || v2) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
},
isAnAdmin: (value, options) => {
if(value === 'true' || value === true){
return options.fn(this);
}
return options.inverse(this);
},
paymentMessage: (status) => {
if(status === 'Paid'){
return '<h2 class="text-success">Your payment has been successfully processed</h2>';
}
if(status === 'Pending'){
const paymentConfig = common.getPaymentConfig();
if(config.paymentGateway === 'instore'){
return `<h2 class="text-warning">${paymentConfig.resultMessage}</h2>`;
}
return '<h2 class="text-warning">The payment for this order is pending. We will be in contact shortly.</h2>';
}
return '<h2 class="text-danger">Your payment has failed. Please try again or contact us.</h2>';
},
paymentOutcome: (status) => {
if(status === 'Paid' || status === 'Pending'){
return '<h5 class="text-warning">Please retain the details above as a reference of payment</h5>';
}
return '';
},
upperFirst: (value) => {
if(value){
return value.replace(/^\w/, (chr) => {
return chr.toUpperCase();
});
}
return value;
},
math: (lvalue, operator, rvalue, options) => {
lvalue = parseFloat(lvalue);
rvalue = parseFloat(rvalue);
return {
'+': lvalue + rvalue,
'-': lvalue - rvalue,
'*': lvalue * rvalue,
'/': lvalue / rvalue,
'%': lvalue % rvalue
}[operator];
},
showCartButtons: (cart) => {
if(!cart){
return 'd-none';
}
return '';
},
snip: (text) => {
if(text && text.length > 155){
return text.substring(0, 155) + '...';
}
return text;
},
fixTags: (html) => {
html = html.replace(/&gt;/g, '>');
html = html.replace(/&lt;/g, '<');
return html;
},
feather: (icon) => {
// eslint-disable-next-line keyword-spacing
return `<svg
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-${icon}"
>
<use xlink:href="/dist/feather-sprite.svg#${icon}"/>
</svg>`;
}
}
});
// session store
const store = new MongoStore({
uri: getDbUri(config.databaseConnectionString),
collection: 'sessions'
});
// Setup secrets
if(!config.secretCookie || config.secretCookie === ''){
const randomString = crypto.randomBytes(20).toString('hex');
config.secretCookie = randomString;
common.updateConfigLocal({ secretCookie: randomString });
}
if(!config.secretSession || config.secretSession === ''){
const randomString = crypto.randomBytes(20).toString('hex');
config.secretSession = randomString;
common.updateConfigLocal({ secretSession: randomString });
}
app.enable('trust proxy');
app.use(helmet());
app.set('port', process.env.PORT || 1111);
app.use(logger('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser(config.secretCookie));
app.use(session({
resave: true,
saveUninitialized: true,
secret: config.secretSession,
cookie: {
path: '/',
httpOnly: true,
maxAge: 900000
},
store: store
}));
app.use(bodyParser.json({
// Only on Stripe URL's which need the rawBody
verify: (req, res, buf) => {
if(req.originalUrl === '/stripe/subscription_update'){
req.rawBody = buf.toString();
}
}
}));
// Set locales from session
app.use(i18n.init);
// serving static content
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'views', 'themes')));
app.use(express.static(path.join(__dirname, 'node_modules', 'feather-icons')));
// Make stuff accessible to our router
app.use((req, res, next) => {
req.handlebars = handlebars;
next();
});
// Ran on all routes
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, no-store');
next();
});
// Setup the routes
app.use('/', index);
app.use('/', customer);
app.use('/', product);
app.use('/', order);
app.use('/', user);
app.use('/', admin);
// Payment route
app.use(`/${config.paymentGateway}`, paymentRoute);
// catch 404 and forward to error handler
app.use((req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if(app.get('env') === 'development'){
app.use((err, req, res, next) => {
console.error(colors.red(err.stack));
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err,
helpers: handlebars.helpers
});
});
}
// production error handler
// no stacktraces leaked to user
app.use((err, req, res, next) => {
console.error(colors.red(err.stack));
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {},
helpers: handlebars.helpers
});
});
// Nodejs version check
const nodeVersionMajor = parseInt(process.version.split('.')[0].replace('v', ''));
if(nodeVersionMajor < 7){
console.log(colors.red(`Please use Node.js version 7.x or above. Current version: ${nodeVersionMajor}`));
process.exit(2);
}
app.on('uncaughtException', (err) => {
console.error(colors.red(err.stack));
process.exit(2);
});
initDb(config.databaseConnectionString, async (err, db) => {
// On connection error we display then exit
if(err){
console.log(colors.red('Error connecting to MongoDB: ' + err));
process.exit(2);
}
// add db to app for routes
app.db = db;
app.config = config;
app.port = app.get('port');
// Fire up the cron job to clear temp held stock
cron.schedule('*/1 * * * *', async () => {
const validSessions = await db.sessions.find({}).toArray();
const validSessionIds = [];
_.forEach(validSessions, (value) => {
validSessionIds.push(value._id);
});
// Remove any invalid cart holds
await db.cart.deleteMany({
sessionId: { $nin: validSessionIds }
});
});
// Set trackStock for testing
if(process.env.NODE_ENV === 'test'){
config.trackStock = true;
}
// Process schemas
await addSchemas();
// We index when not in test env
if(process.env.NODE_ENV !== 'test'){
try{
await runIndexing(app);
}catch(ex){
console.error(colors.red('Error setting up indexes:' + ex.message));
}
}
// Start the app
try{
await app.listen(app.get('port'));
app.emit('appStarted');
if(process.env.NODE_ENV !== 'test'){
console.log(colors.green('expressCart running on host: http://localhost:' + app.get('port')));
}
}catch(ex){
console.error(colors.red('Error starting expressCart app:' + ex.message));
process.exit(2);
}
});
module.exports = app;