diff --git a/package.json b/package.json index 8050b5e..62721b4 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "tailwindcss": "^3.0.7" }, "dependencies": { + "@constemi/itsdangerjs": "^0.0.2", "bookshelf": "^1.2.0", "dotenv": "^10.0.0", "express": "^4.17.2", diff --git a/server/index.js b/server/index.js index cd8bf43..fa8c795 100644 --- a/server/index.js +++ b/server/index.js @@ -1,6 +1,7 @@ const express = require('express') const bodyParser = require('body-parser') const path = require('path') +const { URLSafeTimedSerializer } = require("@constemi/itsdangerjs") require('dotenv').config() @@ -15,6 +16,20 @@ if (process.env.DEBUG || process.env.CROWDFUNDING_SITE_DEBUG) { 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 @@ -213,9 +228,113 @@ router.post('/pledge', async (req, res) => { 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! @@ -228,5 +347,6 @@ module.exports = { knex, bookshelf, Pledge, + UnverifiedPledge, router, } diff --git a/yarn.lock b/yarn.lock index 2d01f5e..e974599 100644 --- a/yarn.lock +++ b/yarn.lock @@ -205,6 +205,11 @@ "@babel/helper-validator-identifier" "^7.15.7" to-fast-properties "^2.0.0" +"@constemi/itsdangerjs@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@constemi/itsdangerjs/-/itsdangerjs-0.0.2.tgz#1cb5803b26fb1262150d64c90f5451511f431340" + integrity sha512-/TKmvxodKwhI42BIKt7YJQR1xD2bWeUFtxjs4RFbRMHjtWgYn5WocyclbkM0WEDY0xoS16eQQxykwdtzYhoCfw== + "@iarna/toml@^2.2.0": version "2.2.5" resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"