From a381077b83fdb2951a202b4844cab4cf7e14bf8e Mon Sep 17 00:00:00 2001 From: Hippo Date: Wed, 5 Jan 2022 23:42:28 +0530 Subject: [PATCH] Set up "email verification" workflow (minus the email itself) We'll start sending out the email in the next step, when the setup of nodemailer is complete. Right now, everything else works: creating a verification link, processing it when clicked on, and even updating (instead of appending) a pledge when the same email submits multiple times. No pretty error messages though; that's another thing to be worked on :P WARNING: Don't deploy this commit live; people will be told about a verification email but they won't actually get it yet! --- package.json | 1 + server/index.js | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 5 ++ 3 files changed, 126 insertions(+) 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"