const express = require('express') 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() } } // 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': req.query.email, 'amount': amount, }, }) .orderBy('created_at', 'DESC') .fetch()) } catch(e) { if (e.name == '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')))}` // TODO: send out the email, along with existing pledge deets console.debug(`Verification link generated: ${verificationLink}`) 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() // dummy message res.send('all done (not)') }) 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, }