Email confirmation and templates

This commit is contained in:
Richard Ramos 2019-11-14 16:01:31 -04:00
parent 0bf6b9d487
commit 62515ce825
10 changed files with 150 additions and 51 deletions

View File

@ -1,6 +1,5 @@
const Events = require("events");
const stripHexPrefix = require("strip-hex-prefix");
const { isSignatureValid } = require("./utils");
const { isSignatureValid, getToken, hexValidator } = require("./utils");
const express = require("express");
const { check, validationResult } = require("express-validator");
const cors = require("cors");
@ -8,11 +7,13 @@ const helmet = require("helmet");
const rateLimit = require("../middleware/rate-limit");
const config = require("../config");
const Database = require("../database");
const events = new Events();
const Subscriber = require("../models/subscriber");
const Subscribers = require("../models/subscribers");
const Verifications = require("../models/verifications");
const Mailer = require("../mail/sendgrid");
const DappConfig = require("../config/dapps");
const cryptoRandomString = require("crypto-random-string");
const events = new Events();
const dappConfig = new DappConfig();
const mailer = new Mailer(config);
@ -20,23 +21,6 @@ const db = new Database(events, config);
db.init();
const getToken = () => {
const expirationTime = new Date();
expirationTime.setUTCHours(expirationTime.getUTCHours() + 2);
return {
token: cryptoRandomString({ length: 150, type: "url-safe" }),
expirationTime
};
};
const hexValidator = value => {
const regex = /^[0-9A-Fa-f]*$/g;
if (regex.test(stripHexPrefix(value))) {
return true;
}
throw new Error("Invalid hex string");
};
events.on("db:connected", () => {
const app = express();
@ -85,7 +69,7 @@ events.on("db:connected", () => {
// TODO: handle subscriptions to particular events
try {
const subscriber = await Subscriber.findOne({
const subscriber = await Subscribers.findOne({
dappId,
address
});
@ -93,11 +77,15 @@ events.on("db:connected", () => {
const t = getToken();
if (!subscriber) {
await Subscriber.create({
const s = await Subscribers.create({
dappId,
email,
address,
verificationTokens: [t]
address
});
await Verifications.create({
...t,
subscriber: s._id
});
} else if (!subscriber.isVerified) {
const d = new Date(subscriber.lastSignUpAttempt);
@ -111,8 +99,12 @@ events.on("db:connected", () => {
}
subscriber.lastSignUpAttempt = d;
subscriber.verificationTokens.push(t);
subscriber.save();
await subscriber.save();
await Verification.create({
...t,
subscriber: subscriber._id
});
}
if (!subscriber || !subscriber.isVerified) {
@ -126,7 +118,6 @@ events.on("db:connected", () => {
}
);
}
} catch (err) {
// TODO: add global error handler
return res.status(400).send(err.message);
@ -167,7 +158,7 @@ events.on("db:connected", () => {
// TODO: handle unsubscribe to particular events
try {
await Subscriber.deleteOne({
await Subscribers.deleteOne({
dappId,
address
});
@ -180,8 +171,33 @@ events.on("db:connected", () => {
}
);
app.get("/confirm/:token", (req, res) => {
// TODO:
app.get("/confirm/:token", [check("token").exists()], async (req, res) => {
const {
params: { token }
} = req;
const verification = await Verifications.findOne({
token
}).populate("subscriber");
if (verification) {
if (verification.expirationTime < new Date()) {
return res.status(400).send("Verification token already expired");
}
if (!verification.subscriber.isVerified) {
verification.subscriber.isVerified = true;
await verification.subscriber.save();
}
await Verifications.deleteMany({
subscriber: verification.subscriber._id
});
} else {
return res.status(400).send("Invalid verification token");
}
return res.status(200).send("OK");
});
app.get("/", (req, res) => res.status(200).json({ ok: true }));

View File

@ -1,5 +1,7 @@
const web3EthAccounts = require("web3-eth-accounts");
const web3Utils = require("web3-utils");
const cryptoRandomString = require("crypto-random-string");
const stripHexPrefix = require("strip-hex-prefix");
const isSignatureValid = (address, message, signature) => {
const accounts = new web3EthAccounts();
@ -9,6 +11,25 @@ const isSignatureValid = (address, message, signature) => {
return address === recoverAddress;
};
module.exports = {
isSignatureValid
const getToken = () => {
const expirationTime = new Date();
expirationTime.setUTCHours(expirationTime.getUTCHours() + 2);
return {
token: cryptoRandomString({ length: 200, type: "url-safe" }),
expirationTime
};
};
const hexValidator = value => {
const regex = /^[0-9A-Fa-f]*$/g;
if (regex.test(stripHexPrefix(value))) {
return true;
}
throw new Error("Invalid hex string");
};
module.exports = {
isSignatureValid,
getToken,
hexValidator
};

View File

@ -1 +1,2 @@
<p>Welcome to teller network!</p>
<h3>Welcome to teller network!</h3>
<p>Verify your email at TODO: {{ token }}</p>

View File

@ -1 +1,2 @@
Welcome to teller network!
Welcome to teller network!
Verify your email at TODO: {{ token }}

View File

@ -1,6 +1,5 @@
const sgMail = require("@sendgrid/mail");
const path = require("path");
const fs = require("fs");
const Handlebars = require("handlebars");
class SendGridMailer {
constructor(config) {
@ -8,12 +7,15 @@ class SendGridMailer {
}
send(template, from, data) {
// TODO: data should be used for templating
const tplText = Handlebars.compile(template.text);
const tplHtml = Handlebars.compile(template.html);
const msg = {
to: data.email,
from,
...template
...template,
text: tplText(data),
html: tplHtml(data)
};
sgMail.send(msg);

View File

@ -3,12 +3,6 @@ const Schema = mongoose.Schema;
const validator = require("validator");
const VerificationSchema = new Schema({
token: String,
expirationTime: Date
});
const SubscriberSchema = new Schema({
id: Schema.Types.ObjectId,
dappId: {
@ -50,13 +44,11 @@ const SubscriberSchema = new Schema({
isVerified: {
type: Boolean,
default: false
},
verificationTokens: [VerificationSchema]
}
});
SubscriberSchema.statics.findVerifiedUsersByDapp = function(dappId) {
return this.find({ dappId, isVerified: true });
};
module.exports = mongoose.model("Subscribers", SubscriberSchema);
module.exports = mongoose.model("Subscribers", SubscriberSchema);

13
models/verifications.js Normal file
View File

@ -0,0 +1,13 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const VerificationSchema = new Schema({
token: String,
expirationTime: Date,
subscriber: {
type: mongoose.Schema.Types.ObjectId,
ref: "Subscribers"
}
});
module.exports = mongoose.model("Verifications", VerificationSchema);

View File

@ -17,6 +17,7 @@
"express": "^4.17.1",
"express-rate-limit": "^5.0.0",
"express-validator": "^6.2.0",
"handlebars": "^4.5.2",
"helmet": "^3.21.2",
"mongoose": "5.7.5",
"strip-hex-prefix": "^1.0.0",

View File

@ -5,7 +5,7 @@ const Ethereum = require("./ethereum");
const { addressCompare } = require("./utils");
const Mailer = require("../mail/sendgrid");
const DappConfig = require("../config/dapps");
const Subscribers = require("../models/subscriber");
const Subscribers = require("../models/subscribers");
const events = new Events();
const dappConfig = new DappConfig();

View File

@ -419,6 +419,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@~2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
@ -1233,6 +1238,17 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
handlebars@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.2.tgz#5a4eb92ab5962ca3415ac188c86dc7f784f76a0f"
integrity sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==
dependencies:
neo-async "^2.6.0"
optimist "^0.6.1"
source-map "^0.6.1"
optionalDependencies:
uglify-js "^3.1.4"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@ -1720,6 +1736,11 @@ minimist@^1.2.0:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
@ -1836,6 +1857,11 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo-async@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
next-tick@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
@ -1900,6 +1926,14 @@ once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
dependencies:
minimist "~0.0.1"
wordwrap "~0.0.2"
p-cancelable@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
@ -2353,6 +2387,11 @@ sliced@1.0.1:
resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=
source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@ -2550,6 +2589,14 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
uglify-js@^3.1.4:
version "3.6.9"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.9.tgz#85d353edb6ddfb62a9d798f36e91792249320611"
integrity sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==
dependencies:
commander "~2.20.3"
source-map "~0.6.1"
ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
@ -2911,6 +2958,11 @@ web3@^1.2.2:
typedarray-to-buffer "^3.1.5"
yaeti "^0.0.6"
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"