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!
This commit is contained in:
Hippo 2022-01-05 23:42:28 +05:30
parent a33ba1dc9c
commit a381077b83
3 changed files with 126 additions and 0 deletions

View file

@ -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",

View file

@ -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,
}

View file

@ -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"