Trigger email verification workflow on API usage

refs: TryGhost/Toolbox#166

The new VerificationTrigger listens to events form the members repository, and will cause the verification workflow to be triggered if the number of events is greater than the configured threshold in a rolling 30 day window.

The importer also no longer depends on the import threshold, so the threshold testing is now done in the processImport method in Ghost - seems sensible since we already had this wrapper and the logic is now tiny, since it's just relying on @tryghost/verification-trigger to handle the real stuff.
This commit is contained in:
Sam Lord 2022-01-27 14:06:29 +00:00 committed by GitHub
parent 5713dfe7f7
commit 08829a6f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 100 deletions

View File

@ -1,3 +1,4 @@
const _ = require('lodash');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const MembersSSR = require('@tryghost/members-ssr');
@ -12,9 +13,9 @@ const labsService = require('../../../shared/labs');
const settingsCache = require('../../../shared/settings-cache');
const config = require('../../../shared/config');
const models = require('../../models');
const _ = require('lodash');
const {GhostMailer} = require('../mail');
const jobsService = require('../jobs');
const VerificationTrigger = require('@tryghost/verification-trigger');
const messages = {
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
@ -32,23 +33,15 @@ const membersConfig = new MembersConfigProvider({
urlUtils
});
const membersStats = new MembersStats({
db: db,
settingsCache: settingsCache,
isSQLite: config.get('database:client') === 'sqlite3'
});
let membersApi;
let membersSettings;
/**
* @description Calculates threshold based on following formula
* Threshold = max{[current number of members], [volume threshold]}
*
* @returns {Promise<number>}
*/
const fetchImportThreshold = async () => {
const membersTotal = await module.exports.stats.getTotalMembers();
const configThreshold = _.get(config.get('hostSettings'), 'emailVerification.importThreshold');
const volumeThreshold = (configThreshold === undefined) ? Infinity : configThreshold;
const threshold = Math.max(membersTotal, volumeThreshold);
return threshold;
};
let verificationTrigger;
const membersImporter = new MembersCSVImporter({
storagePath: config.getContentPath('data'),
@ -58,53 +51,20 @@ const membersImporter = new MembersCSVImporter({
isSet: labsService.isSet.bind(labsService),
addJob: jobsService.addJob.bind(jobsService),
knex: db.knex,
urlFor: urlUtils.urlFor.bind(urlUtils),
fetchThreshold: fetchImportThreshold
urlFor: urlUtils.urlFor.bind(urlUtils)
});
const startEmailVerification = async (importedNumber) => {
const isVerifiedEmail = config.get('hostSettings:emailVerification:verified') === true;
if ((!isVerifiedEmail)) {
// Only trigger flag change and escalation email the first time
if (settingsCache.get('email_verification_required') !== true) {
await models.Settings.edit([{
key: 'email_verification_required',
value: true
}], {context: {internal: true}});
const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
const fromAddress = config.get('user_email');
if (escalationAddress) {
ghostMailer.send({
subject: 'Email needs verification',
html: tpl(messages.emailVerificationEmailMessage, {
importedNumber,
siteUrl: urlUtils.getSiteUrl()
}),
forceTextContent: true,
from: fromAddress,
to: escalationAddress
});
}
}
throw new errors.ValidationError({
message: tpl(messages.emailVerificationNeeded)
});
}
};
const processImport = async (options) => {
const result = await membersImporter.process(options);
const freezeTriggered = result.meta.freeze;
const importSize = result.meta.originalImportSize;
delete result.meta.freeze;
delete result.meta.originalImportSize;
if (freezeTriggered) {
await startEmailVerification(importSize);
const importThreshold = await verificationTrigger.getImportThreshold();
if (importThreshold > importSize) {
await verificationTrigger.startVerificationProcess({
amountImported: importSize,
throwOnTrigger: true
});
}
return result;
@ -139,6 +99,32 @@ module.exports = {
});
}
verificationTrigger = new VerificationTrigger({
configThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
sendVerificationEmail: ({subject, message, amountImported}) => {
const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
const fromAddress = config.get('user_email');
if (escalationAddress) {
this._ghostMailer.send({
subject,
html: tpl(message, {
amountImported,
siteUrl: this._urlUtils.getSiteUrl()
}),
forceTextContent: true,
from: fromAddress,
to: escalationAddress
});
}
},
membersStats,
Settings: models.Settings,
eventRepository: membersApi.events
});
(async () => {
try {
const collection = await models.SingleUseToken.fetchAll();
@ -181,11 +167,7 @@ module.exports = {
processImport: processImport,
stats: new MembersStats({
db: db,
settingsCache: settingsCache,
isSQLite: config.get('database:client') === 'sqlite3'
})
stats: membersStats
};
module.exports.middleware = require('./middleware');

View File

@ -80,8 +80,8 @@
"@tryghost/limit-service": "1.0.9",
"@tryghost/logging": "2.0.2",
"@tryghost/magic-link": "1.0.17",
"@tryghost/members-api": "4.8.1",
"@tryghost/members-importer": "0.4.1",
"@tryghost/members-api": "4.8.3",
"@tryghost/members-importer": "0.5.0",
"@tryghost/members-offers": "0.10.6",
"@tryghost/members-ssr": "1.0.19",
"@tryghost/members-stripe-service": "0.6.5",
@ -103,6 +103,7 @@
"@tryghost/update-check-service": "0.3.0",
"@tryghost/url-utils": "2.0.5",
"@tryghost/validator": "0.1.11",
"@tryghost/verification-trigger": "0.1.0",
"@tryghost/version": "0.1.9",
"@tryghost/vhost-middleware": "1.0.20",
"@tryghost/zip": "1.1.19",

View File

@ -1507,34 +1507,34 @@
jsonwebtoken "^8.5.1"
lodash "^4.17.15"
"@tryghost/member-analytics-service@^0.1.7":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@tryghost/member-analytics-service/-/member-analytics-service-0.1.7.tgz#eab4d17ec747007f007d0a2a8e2e157ac681cfbd"
integrity sha512-WMLxnvBVJiiV1Alef3nvBb6Jk8RiPJxoLEwUrbTT9mQydtQwIYVvh8wsmw+NgMZUKe+zqvwTLyBzqbMHkTWPjw==
"@tryghost/member-analytics-service@^0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@tryghost/member-analytics-service/-/member-analytics-service-0.1.8.tgz#8f43e1fd2ee661a2ab3b7469da7213e2a8886225"
integrity sha512-lDXIp0rrPC30eemita5h1C9wd4M5fzJft0v/346xyBCaIK2As+MUN5IuZG3RIqXG9wLevnFzZ3mNUXBLuGxEEA==
dependencies:
"@tryghost/domain-events" "^0.1.6"
"@tryghost/errors" "^0.2.14"
"@tryghost/member-events" "^0.3.3"
"@tryghost/member-events" "^0.3.4"
"@tryghost/tpl" "^0.1.4"
bson-objectid "^2.0.1"
"@tryghost/member-events@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@tryghost/member-events/-/member-events-0.3.3.tgz#96da2ffe762a30571e4bad4c75488f20681e8730"
integrity sha512-Mi94RMnGJFL0hzhPWpYVAfQBAG4eIBcaJR4Dghy2s6wfUGCaV3iZftyNcHvmCezN5tfrLAmQt6b/fI+NYrK/9Q==
"@tryghost/member-events@^0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@tryghost/member-events/-/member-events-0.3.4.tgz#8aadea01d67660e6bce49172215c25857ce5ef32"
integrity sha512-hPmk3RA/Vs6exfkz8jdA52Lx2KFN8XsZHR694MUEE7tU6FHTbjQp0VGn1PZXTU/UmVJV6xDyUbyXeVsaGvu09w==
"@tryghost/members-analytics-ingress@^0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@tryghost/members-analytics-ingress/-/members-analytics-ingress-0.1.8.tgz#648b3fa6dee947d0c06892f19f6ddaa5e896a4fc"
integrity sha512-RTd+W5BcGFqYI1/ArzFnZwFDs62FwLnmtvJO4dOfP5i1dY0vtwqbqDZNciHXa2fW9IAP4PLiCiyN4MrppqI6xw==
"@tryghost/members-analytics-ingress@^0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@tryghost/members-analytics-ingress/-/members-analytics-ingress-0.1.9.tgz#769d6d2f7b4b4e55ae12d8ef170974f5a25ffafe"
integrity sha512-J0mzLkXycEuunugc+1KX9vPMvesCeJ7nLXLlT/bf7BRBov4hDjthBhAAs7XIciyGZXx0hleuh0EAVYZUnwlHhg==
dependencies:
"@tryghost/domain-events" "^0.1.6"
"@tryghost/member-events" "^0.3.3"
"@tryghost/member-events" "^0.3.4"
"@tryghost/members-api@4.8.1":
version "4.8.1"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-4.8.1.tgz#f3f6e4b5373407bc9c73275edf7aeb3007d4b446"
integrity sha512-tAmdwrtFwB8DbezSySIL1GhumVpfCi4b2FIfQo4eTtfyEGiUdoD5JRR+ToNbxor1/h+HN0gETnteflmQ97mOEw==
"@tryghost/members-api@4.8.3":
version "4.8.3"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-4.8.3.tgz#b85c095e2ac4477fe554c44607dec26681592aa9"
integrity sha512-7YOF65FWTICfWDuCwlRJmjVhzX4rRM4gjig9Hx9nxuRosOusv45B88RpbWqB4b7i/OfdMUvvVSemlGXjx8fPOA==
dependencies:
"@nexes/nql" "^0.6.0"
"@tryghost/debug" "^0.1.2"
@ -1542,11 +1542,11 @@
"@tryghost/errors" "^1.1.1"
"@tryghost/logging" "^2.0.0"
"@tryghost/magic-link" "^1.0.17"
"@tryghost/member-analytics-service" "^0.1.7"
"@tryghost/member-events" "^0.3.3"
"@tryghost/members-analytics-ingress" "^0.1.8"
"@tryghost/member-analytics-service" "^0.1.8"
"@tryghost/member-events" "^0.3.4"
"@tryghost/members-analytics-ingress" "^0.1.9"
"@tryghost/members-payments" "^0.1.8"
"@tryghost/members-stripe-service" "^0.6.4"
"@tryghost/members-stripe-service" "^0.6.5"
"@tryghost/tpl" "^0.1.2"
"@types/jsonwebtoken" "^8.5.1"
bluebird "^3.5.4"
@ -1570,10 +1570,10 @@
papaparse "5.3.1"
pump "^3.0.0"
"@tryghost/members-importer@0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@tryghost/members-importer/-/members-importer-0.4.1.tgz#e5c64ac94f03b922c9bb4067eeb21945373637c8"
integrity sha512-8vpei+dHRRosS1UpkyPDI/3100kFMdEidxH8hDFEZbyQjphGmQdeKpi04yJUQFGeANKBNljiifOuZ9vJdJcd9g==
"@tryghost/members-importer@0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-importer/-/members-importer-0.5.0.tgz#262b542bd597853b091b819adcd8a13c7bd2dc0a"
integrity sha512-WNLexqKyqJ6yyBSoZWhtacJwWEoG6PlPxaCwjAk2pifPfWuT+T1uVk+tm7ETmQzBLxf0HKpcdpN4j4w9J0A0Ew==
dependencies:
"@tryghost/errors" "^0.2.13"
"@tryghost/members-csv" "^1.2.3"
@ -1609,7 +1609,7 @@
jsonwebtoken "^8.5.1"
lodash "^4.17.11"
"@tryghost/members-stripe-service@0.6.5":
"@tryghost/members-stripe-service@0.6.5", "@tryghost/members-stripe-service@^0.6.5":
version "0.6.5"
resolved "https://registry.yarnpkg.com/@tryghost/members-stripe-service/-/members-stripe-service-0.6.5.tgz#68e6bc4d3f5e4ee61468f5fc43eb77650543bf63"
integrity sha512-cTFcacy/Rst+ao4oRIszCMUw8oI8hKo1u3xxbjXjchDQUfVAUxxfnDoHcULH1d4mXc+LdftD2iKYNav/BATC5Q==
@ -1619,16 +1619,6 @@
leaky-bucket "^2.2.0"
stripe "^8.174.0"
"@tryghost/members-stripe-service@^0.6.4":
version "0.6.4"
resolved "https://registry.yarnpkg.com/@tryghost/members-stripe-service/-/members-stripe-service-0.6.4.tgz#8946e1eb8becaccb0c1dc3a7d26a6931802d2023"
integrity sha512-twxt5r/yqSGFO48cV6RtMyTngd+HvBBr97/Psbjy5TnyYZo6D1AXetocHscOMqbWkpu3CSTBZgJx0VqywKLYKQ==
dependencies:
"@tryghost/debug" "^0.1.4"
"@tryghost/errors" "1.2.0"
leaky-bucket "^2.2.0"
stripe "^8.174.0"
"@tryghost/metrics@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tryghost/metrics/-/metrics-1.0.3.tgz#091ff6d0cc6c0da05bab6ba8231312cae3d395de"
@ -1878,6 +1868,14 @@
moment-timezone "0.5.23"
validator "7.2.0"
"@tryghost/verification-trigger@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@tryghost/verification-trigger/-/verification-trigger-0.1.0.tgz#7d1baf19c26e09c51f8921ad1e7fce20d562b286"
integrity sha512-k+nFYrQqn9gQbI9N7aaIKo5+/ot7l6IzsgJ64BqN+X4/25g01x2zAHbKk4fwTcMQ9sOFstUqBPAr4thZgHIHUg==
dependencies:
"@tryghost/domain-events" "^0.1.6"
"@tryghost/member-events" "^0.3.4"
"@tryghost/version@0.1.9", "@tryghost/version@^0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@tryghost/version/-/version-0.1.9.tgz#47474fc04675028d5e178c73d135c138165802a1"