3c5cf21274
refs: https://github.com/TryGhost/Toolbox/issues/166 New package handles the email verification workflow to prevent spammers. It currently handles MembersSubscribeEvent to detect potential abuse of the API to add members, and exposes methods for checking the threshold / starting the verification process for use by other areas of the code (at the moment - just member imports). The import package no longer needs to handle anything related to verification since it can be handled in the wrapper function in Ghost, and the API package doesn't need to do anything other than dispatch the new event.
116 lines
4.7 KiB
JavaScript
116 lines
4.7 KiB
JavaScript
const errors = require('@tryghost/errors');
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
const {MemberSubscribeEvent} = require('@tryghost/member-events');
|
|
|
|
const messages = {
|
|
emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
|
|
emailVerificationEmailSubject: `Email needs verification`,
|
|
emailVerificationEmailMessage: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`
|
|
};
|
|
|
|
class VerificationTrigger {
|
|
/**
|
|
*
|
|
* @param {object} deps
|
|
* @param {number} deps.configThreshold Threshold for triggering verification as defined in config
|
|
* @param {() => boolean} deps.isVerified Check Ghost config to see if we are already verified
|
|
* @param {() => boolean} deps.isVerificationRequired Check Ghost settings to see whether verification has been requested
|
|
* @param {(content: {subject: string, message: string, amountImported: number}) => {}} deps.sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
|
|
* @param {any} deps.membersStats MemberStats service
|
|
* @param {any} deps.Settings Ghost Settings model
|
|
* @param {any} deps.eventRepository For querying events
|
|
*/
|
|
constructor({
|
|
configThreshold,
|
|
isVerified,
|
|
isVerificationRequired,
|
|
sendVerificationEmail,
|
|
membersStats,
|
|
Settings,
|
|
eventRepository
|
|
}) {
|
|
this._configThreshold = configThreshold;
|
|
this._isVerified = isVerified;
|
|
this._isVerificationRequired = isVerificationRequired;
|
|
this._sendVerificationEmail = sendVerificationEmail;
|
|
this._membersStats = membersStats;
|
|
this._Settings = Settings;
|
|
this._eventRepository = eventRepository;
|
|
|
|
DomainEvents.subscribe(MemberSubscribeEvent, async (event) => {
|
|
if (event.data.source === 'api' && isFinite(this._configThreshold)) {
|
|
const createdAt = new Date();
|
|
createdAt.setDate(createdAt.getDate() - 30);
|
|
const events = await this._eventRepository.getNewsletterSubscriptionEvents({
|
|
// Date in last 30 days, source is API
|
|
filter: `source:api+created_at:>'${createdAt.toISOString().replace('T', ' ').substring(0, 19)}'`
|
|
});
|
|
|
|
if (events.meta.pagination.total > this._configThreshold) {
|
|
await this.startVerificationProcess({
|
|
amountImported: events.meta.pagination.total,
|
|
throwOnTrigger: false
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async getImportThreshold() {
|
|
const volumeThreshold = this._configThreshold;
|
|
if (isFinite(volumeThreshold)) {
|
|
const membersTotal = await this._membersStats.getTotalMembers();
|
|
return Math.max(membersTotal, volumeThreshold);
|
|
} else {
|
|
return volumeThreshold;
|
|
}
|
|
}
|
|
|
|
/** @typedef IVerificationResult
|
|
* @property {boolean} needsVerification Whether the verification workflow was triggered
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {object} config
|
|
* @param {number} config.amountImported Amount of members which were imported
|
|
* @param {boolean} config.throwOnTrigger Whether to throw if verification is needed
|
|
* @returns {Promise<IVerificationResult>} Object containing property "needsVerification" - true when triggered
|
|
*/
|
|
async startVerificationProcess({
|
|
amountImported,
|
|
throwOnTrigger
|
|
}) {
|
|
if (!this._isVerified()) {
|
|
// Only trigger flag change and escalation email the first time
|
|
if (!this._isVerificationRequired()) {
|
|
await this._Settings.edit([{
|
|
key: 'email_verification_required',
|
|
value: true
|
|
}], {context: {internal: true}});
|
|
|
|
this._sendVerificationEmail({
|
|
message: messages.emailVerificationEmailMessage,
|
|
subject: messages.emailVerificationEmailSubject,
|
|
amountImported
|
|
});
|
|
|
|
if (throwOnTrigger) {
|
|
throw new errors.ValidationError({
|
|
message: messages.emailVerificationNeeded
|
|
});
|
|
}
|
|
|
|
return {
|
|
needsVerification: true
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
needsVerification: false
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = VerificationTrigger; |