7cca56eaca
Also made sure that these columns are auto-added to old
databases 😉
558 lines
16 KiB
JavaScript
558 lines
16 KiB
JavaScript
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')
|
|
|
|
t.string('referral_code', 256)
|
|
t.string('referrer', 128)
|
|
}
|
|
|
|
// function to make sure referral columns exist
|
|
// we need this because they were added later in the code, so
|
|
// if there are old databases they'd need to be updated.
|
|
|
|
function ensureReferralColumns(table) {
|
|
knex.schema.hasColumn(table, 'referral_code').then(function(exists) {
|
|
if (!exists) {
|
|
console.debug(`No referral_code column in ${table}! Adding now...`)
|
|
knex.schema.alterTable(table, t => {
|
|
t.string('referral_code', 256)
|
|
}).then(r=> console.debug(`referral_code column added to ${table}:`, r))
|
|
}
|
|
})
|
|
|
|
knex.schema.hasColumn(table, 'referrer').then(function(exists) {
|
|
if (!exists) {
|
|
console.debug(`No referrer column in ${table}! Adding it now...`)
|
|
knex.schema.alterTable(table, t => {
|
|
t.string('referrer', 128)
|
|
}).then(r=> console.debug(`referrer column added to ${table}:`, r))
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
} else {
|
|
ensureReferralColumns('pledges')
|
|
}
|
|
})
|
|
|
|
// 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)
|
|
} else {
|
|
ensureReferralColumns('unverified_pledges')
|
|
}
|
|
})
|
|
|
|
// 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, '..', 'src'),
|
|
])
|
|
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
|
|
let { total_people, total_rupees } = (await knex('pledges')
|
|
.sum('amount as total_rupees')
|
|
.count('* as total_people'))[0]
|
|
|
|
|
|
// get latest pledges
|
|
let recentPledges = (
|
|
await Pledge
|
|
.forge()
|
|
.orderBy('created_at', 'DESC')
|
|
.fetchPage({ limit: 5 })
|
|
).models
|
|
|
|
if (DEBUG) console.log(`Listing ${recentPledges.length} pledges`)
|
|
|
|
twing.render('index.htm.twig', {
|
|
'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}%"`,
|
|
'recent_pledges': recentPledges,
|
|
}).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) {
|
|
let output = await twing.render('form-errors.htm.twig', {
|
|
errors: errors,
|
|
})
|
|
|
|
res.send(output)
|
|
return
|
|
}
|
|
|
|
// save if there weren't
|
|
if (DEBUG) console.debug (`Saving pledge: ${JSON.stringify(pledge)}`)
|
|
try {
|
|
await pledge.save()
|
|
} catch (err) {
|
|
|
|
let output = await twing.render('error.htm.twig', {
|
|
error: "Sorry, something went wrong while saving your pledge and we don't know what in was. This website is still in beta so we do have a glitches once in a while. Apologies and please try again...🙁",
|
|
})
|
|
|
|
res.send(output)
|
|
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`
|
|
|
|
// to make things snappier, we don't `await` for the sending to finish.
|
|
let receipt = sendMail({
|
|
to: req.body.email,
|
|
subject: `Finish ${existingPledge ? 'updating' : 'saving'} your pledge to Snipette`,
|
|
text: text,
|
|
})
|
|
|
|
// return the success page
|
|
twing.render('pledge.htm.twig', {
|
|
'email': pledge.get('email'),
|
|
}).then((output) => {
|
|
res.send(output)
|
|
})
|
|
})
|
|
|
|
// 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') {
|
|
|
|
let output = await twing.render('error.htm.twig', {
|
|
error: "Sorry, looks like your link has expired. Please go back and try pledging again. Hopefully you'll manage it quicker this time 😛",
|
|
})
|
|
|
|
res.send(output)
|
|
return
|
|
} else {
|
|
|
|
let output = await twing.render('error.htm.twig', {
|
|
error: "An unknown error occurred. Please generate a new link and try again. Sorry for the inconvenience 🤦",
|
|
})
|
|
|
|
res.send(output)
|
|
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') {
|
|
|
|
let output = await twing.render('error.htm.twig', {
|
|
error: "That pledge was not found in our records. Are you sure you made it? 🔎",
|
|
})
|
|
|
|
res.send(output)
|
|
return
|
|
} else {
|
|
|
|
let output = await twing.render('error.htm.twig', {
|
|
error: "An unknown error occurred. Please generate a new link and try again. Sorry for the inconvenience 🤦",
|
|
})
|
|
|
|
res.send(output)
|
|
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`
|
|
|
|
// to make things snappier, we don't `await` for the sending to finish
|
|
let receipt = sendMail({
|
|
to: pledge.get('email'),
|
|
subject: `Your pledge has been saved!`,
|
|
text: text,
|
|
})
|
|
|
|
// Send confirmation message
|
|
res.redirect('/thanks')
|
|
})
|
|
|
|
router.get('/thanks', async (req, res) => {
|
|
let output = await twing.render('thanks.htm.twig')
|
|
|
|
res.send(output)
|
|
return
|
|
})
|
|
|
|
router.get('/pledges', async (req, res) => {
|
|
if (DEBUG) console.debug('Returning pledges list')
|
|
|
|
// count people
|
|
let { total_people, total_rupees } = (await knex('pledges')
|
|
.sum('amount as total_rupees')
|
|
.count('* as total_people'))[0]
|
|
|
|
|
|
// get latest pledges
|
|
let recentPledges = (
|
|
await Pledge
|
|
.forge()
|
|
.orderBy('created_at', 'DESC')
|
|
.fetchAll()
|
|
).models
|
|
|
|
if (DEBUG) console.log(`Listing ${recentPledges.length} pledges`)
|
|
|
|
twing.render('pledges.htm.twig', {
|
|
'recent_pledges': recentPledges,
|
|
}).then((output) => {
|
|
res.send(output)
|
|
})
|
|
})
|
|
|
|
router.use(express.static('src/assets'))
|
|
|
|
// 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,
|
|
}
|