const express = require('express') const nodemailer = require('nodemailer') const bodyParser = require('body-parser') const path = require('path') const { URLSafeTimedSerializer } = require("@constemi/itsdangerjs") require('dotenv').config() // set up debug let DEBUG if (process.env.DEBUG || process.env.CROWDFUNDING_SITE_DEBUG) { DEBUG = true } else { DEBUG = false } if (DEBUG) console.log('Starting website in debug mode') // set up secret key let secretKey if (process.env.CROWDFUNDING_SITE_SECRET_KEY) { secretKey = process.env.CROWDFUNDING_SITE_SECRET_KEY } else { if (DEBUG) { secretKey = 'NotReallyASecret' console.warn("Secret key is not set! We're falling back to the default one which is INSECURE and NOT TO BE USED IN PRODUCTION") } else { console.error('No secret key is set. You cannot run this in production without setting a secret key because that would be very insecure. Sorry.') process.exit() } } // set up nodemailer (if configured) let mailer let emailFrom if ( !!process.env.CROWDFUNDING_SITE_EMAIL_HOST && !!process.env.CROWDFUNDING_SITE_EMAIL_USER && !!process.env.CROWDFUNDING_SITE_EMAIL_PASSWORD ) { mailer = nodemailer.createTransport({ host: process.env.CROWDFUNDING_SITE_EMAIL_HOST, port: process.env.CROWDFUNDING_SITE_EMAIL_PORT || 587, secure: process.env.CROWDFUNDING_SITE_EMAIL_SECURE == 'false' ? false : true, auth: { user: process.env.CROWDFUNDING_SITE_EMAIL_USER, pass: process.env.CROWDFUNDING_SITE_EMAIL_PASSWORD, } }) if (!!process.env.CROWDFUNDING_SITE_EMAIL_FROM) { emailFrom = process.env.CROWDFUNDING_SITE_EMAIL_FROM } else { emailFrom = process.env.CROWDFUNDING_SITE_EMAIL_USER console.warn(`No "From" address set! Using ${emailFrom} as a default.`) } } else { console.log("Email has not been configured! Some features won't work properly until this is set up :/") } async function sendMail(message) { if (DEBUG || !mailer) { console.log(`\n\n-------------------------- BEGIN EMAIL --------------------------\n`) console.log(`From: ${message.from}\nTo:${message.to}\nSubject:${message.subject||'(no subject)'}\n\n${message.text}`) console.log(`\n--------------------------- END EMAIL ---------------------------`) return } else { let receipt = await mailer.sendMail({ from: message.from || emailFrom, to: message.to, subject: message.subject, text: message.text, html: message.html, }) return receipt } } // get goal details const goalPeople = Number(process.env.CROWDFUNDING_SITE_GOAL_PEOPLE) || 750 const goalRupees = Number(process.env.CROWDFUNDING_SITE_GOAL_RUPEES) || 500000 // set up knex (for database) const knex = require('knex')({ client:'sqlite3', connection:{ filename:'./donors.sqlite' }, useNullAsDefault: true }) // schema to save pledges let pledgeSchema = function(t) { t.increments('id').primary() t.timestamp('created_at').defaultTo(knex.fn.now()).notNullable() t.boolean('was_robot').defaultTo(true) t.integer('amount').notNullable() t.boolean('overseas') t.string('name', 128) t.boolean('anonymous') t.string('email', 128) t.string('phone', 32) t.integer('retry_times').defaultTo(10) t.boolean('get_newsletter').defaultTo(false) t.text('other_message') } // make sure pledges table exists knex.schema.hasTable('pledges').then(function(exists) { if (!exists) { if (DEBUG) console.debug('No pledge table exists! Creating one now...') return knex.schema.createTable('pledges', pledgeSchema) } }) // make sure unverified pledge table exists knex.schema.hasTable('unverified_pledges').then(function(exists) { if (!exists) { if (DEBUG) console.debug('No unverified pledge table exists! Creating one now...') return knex.schema.createTable('unverified_pledges', pledgeSchema) } }) // set up bookshelf (for easy interface for database) bookshelf = require('bookshelf')(knex) const Pledge = bookshelf.model('Pledge', { tableName: 'pledges', }) const UnverifiedPledge = bookshelf.model('UnverifiedPledge', { tableName: 'unverified_pledges', }) // decide base url and port for app (can be configured) const baseUrl = process.env.CROWDFUNDING_SITE_BASE_URL || '/' const port = process.env.CROWDFUNDING_SITE_PORT || 5000 // set up twing const {TwingEnvironment, TwingLoaderFilesystem} = require('twing') let loader = new TwingLoaderFilesystem(path.resolve(__dirname, '..', 'dist')) let twing = new TwingEnvironment(loader) // set up express const app = express() const router = express.Router() app.use(baseUrl, router) router.use(bodyParser.urlencoded({ extended: true, })) // main views router.get('/', async (req, res) => { if (DEBUG) console.debug('Returning home page') // count people // TODO: optimise to do using SQL only let total_people = 0 let total_rupees = 0 let result = await Pledge.fetchAll() for (let pledge of result.models) { total_people += 1 total_rupees += pledge.get('amount') } twing.render('index.html', { 'goal_rupees': Number(goalRupees).toLocaleString('en-IN'), 'goal_people': Number(goalPeople).toLocaleString('en-IN'), 'progress_rupees': Number(total_rupees).toLocaleString('en-IN'), 'progress_people': Number(total_people).toLocaleString('en-IN'), 'percent_rupees': `style="width: ${total_rupees/goalRupees*100}%"`, 'percent_people': `style="width: ${total_people/goalPeople*100}%"`, }).then((output) => { res.end(output) }) }) // function to validate pledges before saving function validatePledge(body, PledgeModel = Pledge) { // errors get saved here let errors = [] let was_robot = body.was_robot if(was_robot != 'no') { errors.push('Only humans are allowed to donate money. Robots are too digital 🙁') } let amount = body.amount if (!amount || amount == 'custom') { amount = body['amount-custom'] } if (!amount || amount <= 0) { errors.push('Pledge amount too small. Please choose at least a rupee!') } try { amount = Number(amount) } catch (err) { errors.push('Invalid amount. Please choose a positive number!') } let name = body.name if (name.length <=0) { errors.push('What is your name? You can be anonymous to the world but at least we should know...') } let anonymous = body.anonymous == 'on' ? true : false let email = body.email if (email.length < 5) { errors.push('Please enter a valid email address') } let phone = body.phone let retry_times try { retry_times = body.retry_times } catch (err) { errors.push('Invalid retry count. Please choose a positive number!') } let get_newsletter = body.get_newsletter == 'yes' ? true : false let overseas = body.overseas == 'yes' ? true : false let other_message = body.other_message // enter the info let pledge = new PledgeModel() // may be Pledge or UnverifiedPledge pledge.set('was_robot', was_robot) pledge.set('amount', amount) pledge.set('overseas', overseas) pledge.set('name', name) pledge.set('anonymous', anonymous) pledge.set('email', email) pledge.set('phone', phone) pledge.set('retry_times', retry_times) pledge.set('get_newsletter', get_newsletter) pledge.set('other_message', other_message) // return it all! return { pledge: pledge, errors: errors, } } router.post('/pledge', async (req, res) => { if (DEBUG) console.debug('New pledge:', req.body) // check that the right submit button was pressed let submit = req.body.submit if (submit != 'Save Pledge') { errors.push("This request seems to have been tampered with. Are you sure it wasn't you doing the tampering?") } // process the pledge with our handy function let {pledge, errors} = validatePledge(req.body, UnverifiedPledge) // fail if there were errors if (!!errors.length) { res.send(`Errors: ${'' + errors}`) return } // save if there weren't if (DEBUG) console.debug (`Saving pledge: ${JSON.stringify(pledge)}`) try { await pledge.save() } catch (err) { res.send("Sorry, something went wrong while saving your pledge and we don't know what 🙁. Please try again...") return } // check for existing pledge let existingPledge try { existingPledge = await (Pledge .query({ where: { 'email': pledge.get('email'), 'amount': pledge.get('amount'), }, }) .orderBy('created_at', 'DESC') .fetch()) } catch(e) { if (e.message == 'EmptyResponse' && DEBUG) { console.debug('No existing pledge') } else { console.warn(`Weird error happened: ${e}`) } } // generate verification link let serialiser = URLSafeTimedSerializer(secretKey, pledge.get('email')) let verificationLink = `${req.protocol}://${req.hostname}/verify?email=${encodeURIComponent(pledge.get('email'))}&key=${encodeURIComponent(serialiser.dumps(pledge.get('amount')))}` // send out the email, along with existing pledge deets console.debug(`Verification link generated: ${verificationLink}`) let text = `Hi ${pledge.get('name')}, Thank you so much for your pledge of ₹${Number(pledge.get('amount')).toLocaleString('en-IN')} to Snipette! To finish the pledge, please verify your email by clicking the link below: ${verificationLink} If you have any questions, don't hesitate to reach out: you can drop a line anytime to editors@snipettemag.com. Thanks, The Snipette Team` let receipt = await sendMail({ to: req.body.email, subject: `Finish ${existingPledge ? 'updating' : 'saving'} your pledge to Snipette`, text: text, }) res.send("Thank you! We're still working on setting up this website, so as you can see this page doesn't look great at the moment, but we will be sending you a confirmation email in a few days. Watch out for an email from editors@snipettemag.com, and if it doesn't reach, check your spam box :P") }) // save pledge after verification complete router.get('/verify', async (req, res) => { if (DEBUG) console.debug('Validating pledge:', req.query) // unpack verification link (unless it's expired) let serialiser = URLSafeTimedSerializer(secretKey, req.query.email) let amount try { amount = serialiser.loads(req.query.key, 300) // number in seconds } catch(e) { if (e.name == 'SignatureExpired') { res.send("Oops, looks like your link has expired. Please go back and try pledging again. Sorry :(") return } else { res.send("An unknown error occurred. Please generate a new link and try again. Sorry for the inconvenience :(") return } } // check against database let pledge try { pledge = await (UnverifiedPledge .query({ where: { 'email': req.query.email, 'amount': amount, }, }) .orderBy('created_at', 'DESC') .fetch()) } catch(e) { if (e.name == 'EmptyResponse') { res.send('That pledge was not found in our records. Are you sure you made it? Please go back and try again :(') return } else { throw e res.send("An unknown error occurred. Please generate a new link and try again. Sorry for the inconvenience :(") return } } // prepare our new pledge let newPledge = new Pledge() newPledge.set('was_robot', pledge.get('was_robot')) newPledge.set('amount', pledge.get('amount')) newPledge.set('overseas', pledge.get('overseas')) newPledge.set('name', pledge.get('name')) newPledge.set('anonymous', pledge.get('anonymous')) newPledge.set('email', pledge.get('email')) newPledge.set('phone', pledge.get('phone')) newPledge.set('retry_times', pledge.get('retry_times')) newPledge.set('get_newsletter', pledge.get('get_newsletter')) newPledge.set('other_message', pledge.get('other_message')) // destroy previous pledges by that user try { await (Pledge .query({ where: { email: req.query.email, } }) .destroy()) } catch(e) { if (e.message != 'No Rows Deleted') throw e } // save the new pledge await newPledge.save() // send the confirmation email let text = `Hi ${pledge.get('name')}, Your pledge of ₹${Number(pledge.get('amount')).toLocaleString('en-IN')} to Snipette Analog has been saved! Thank you so much for your support. Once we hit our goals, we will contact you to make the final payment. Meanwhile, here are the details of your pledge for reference: Name: ${pledge.get('name')} Pledge amount: ${Number(pledge.get('amount')).toLocaleString('en-IN')}${pledge.overseas ? ' (plus $25 for shipping)' : ''} Email: ${pledge.get('email')} Phone: ${pledge.get('phone') || 'not provided'} Extra message: ${pledge.get('other_message') || '[no message sent]'} We will attempt to contact you ${pledge.get('retry_times') <= 1 ? 'only once' : String(pledge.get('retry_times')) + ' times'} before giving up. You can update your pledge by going to the crowdfunding homepage and saving a pledge with the same email address as you used before. If you have any questions, don't hesitate to reach out: you can drop a line anytime to editors@snipettemag.com. Thanks, The Snipette Team` let receipt = await sendMail({ to: pledge.get('email'), subject: `Your pledge has been saved!`, text: text, }) // Send confirmation message res.send('Thank you! Your pledge has been saved. Watch out for the confirmation email in your inbox :)') }) router.use(express.static('dist')) // start the listener! app.listen(port, () => { console.log(`Server is up at port ${port}`) }) // end note: in case we want to import this somewhere for testing module.exports = { knex, bookshelf, Pledge, UnverifiedPledge, router, }