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 errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl'); const tpl = require('@tryghost/tpl');
const MembersSSR = require('@tryghost/members-ssr'); const MembersSSR = require('@tryghost/members-ssr');
@ -12,9 +13,9 @@ const labsService = require('../../../shared/labs');
const settingsCache = require('../../../shared/settings-cache'); const settingsCache = require('../../../shared/settings-cache');
const config = require('../../../shared/config'); const config = require('../../../shared/config');
const models = require('../../models'); const models = require('../../models');
const _ = require('lodash');
const {GhostMailer} = require('../mail'); const {GhostMailer} = require('../mail');
const jobsService = require('../jobs'); const jobsService = require('../jobs');
const VerificationTrigger = require('@tryghost/verification-trigger');
const messages = { const messages = {
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.', noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
@ -32,23 +33,15 @@ const membersConfig = new MembersConfigProvider({
urlUtils urlUtils
}); });
const membersStats = new MembersStats({
db: db,
settingsCache: settingsCache,
isSQLite: config.get('database:client') === 'sqlite3'
});
let membersApi; let membersApi;
let membersSettings; let membersSettings;
let verificationTrigger;
/**
* @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;
};
const membersImporter = new MembersCSVImporter({ const membersImporter = new MembersCSVImporter({
storagePath: config.getContentPath('data'), storagePath: config.getContentPath('data'),
@ -58,53 +51,20 @@ const membersImporter = new MembersCSVImporter({
isSet: labsService.isSet.bind(labsService), isSet: labsService.isSet.bind(labsService),
addJob: jobsService.addJob.bind(jobsService), addJob: jobsService.addJob.bind(jobsService),
knex: db.knex, knex: db.knex,
urlFor: urlUtils.urlFor.bind(urlUtils), urlFor: urlUtils.urlFor.bind(urlUtils)
fetchThreshold: fetchImportThreshold
}); });
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 processImport = async (options) => {
const result = await membersImporter.process(options); const result = await membersImporter.process(options);
const freezeTriggered = result.meta.freeze;
const importSize = result.meta.originalImportSize; const importSize = result.meta.originalImportSize;
delete result.meta.freeze;
delete result.meta.originalImportSize; delete result.meta.originalImportSize;
if (freezeTriggered) { const importThreshold = await verificationTrigger.getImportThreshold();
await startEmailVerification(importSize); if (importThreshold > importSize) {
await verificationTrigger.startVerificationProcess({
amountImported: importSize,
throwOnTrigger: true
});
} }
return result; 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 () => { (async () => {
try { try {
const collection = await models.SingleUseToken.fetchAll(); const collection = await models.SingleUseToken.fetchAll();
@ -181,11 +167,7 @@ module.exports = {
processImport: processImport, processImport: processImport,
stats: new MembersStats({ stats: membersStats
db: db,
settingsCache: settingsCache,
isSQLite: config.get('database:client') === 'sqlite3'
})
}; };
module.exports.middleware = require('./middleware'); module.exports.middleware = require('./middleware');

View File

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

View File

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