2022-01-27 13:57:51 +03:00
const errors = require ( '@tryghost/errors' ) ;
2022-02-03 18:02:33 +03:00
const DomainEvents = require ( '@tryghost/domain-events' ) ;
2022-10-06 08:50:27 +03:00
const { MemberCreatedEvent } = require ( '@tryghost/member-events' ) ;
2022-01-27 13:57:51 +03:00
const messages = {
2022-08-29 07:18:46 +03:00
emailVerificationNeeded : ` We're hard at work processing your import. To make sure you get great deliverability, 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. ` ,
2022-01-27 13:57:51 +03:00
emailVerificationEmailSubject : ` Email needs verification ` ,
2022-08-25 05:43:46 +03:00
emailVerificationEmailMessageImport : ` Email verification needed for site: {siteUrl}, has imported: {amountTriggered} members in the last 30 days. ` ,
2022-08-25 09:26:26 +03:00
emailVerificationEmailMessageAdmin : ` Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the Admin client in the last 30 days. ` ,
2022-08-25 05:43:46 +03:00
emailVerificationEmailMessageAPI : ` Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the API in the last 30 days. `
2022-01-27 13:57:51 +03:00
} ;
class VerificationTrigger {
/ * *
2022-02-04 14:54:48 +03:00
*
2022-01-27 13:57:51 +03:00
* @ param { object } deps
2023-01-04 13:22:12 +03:00
* @ param { ( ) => number } deps . getApiTriggerThreshold Threshold for triggering API & Import sourced verifications
* @ param { ( ) => number } deps . getAdminTriggerThreshold Threshold for triggering Admin sourced verifications
* @ param { ( ) => number } deps . getImportTriggerThreshold Threshold for triggering Import sourced verifications
2022-01-27 13:57:51 +03:00
* @ 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
2022-11-18 17:05:15 +03:00
* @ param { ( content : { subject : string , message : string , amountTriggered : number } ) => Promise < void > } deps . sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
2022-01-27 13:57:51 +03:00
* @ param { any } deps . Settings Ghost Settings model
* @ param { any } deps . eventRepository For querying events
* /
constructor ( {
2023-01-04 13:22:12 +03:00
getApiTriggerThreshold ,
getAdminTriggerThreshold ,
getImportTriggerThreshold ,
2022-01-27 13:57:51 +03:00
isVerified ,
isVerificationRequired ,
sendVerificationEmail ,
Settings ,
eventRepository
} ) {
2023-01-04 13:22:12 +03:00
this . _getApiTriggerThreshold = getApiTriggerThreshold ;
this . _getAdminTriggerThreshold = getAdminTriggerThreshold ;
this . _getImportTriggerThreshold = getImportTriggerThreshold ;
2022-01-27 13:57:51 +03:00
this . _isVerified = isVerified ;
this . _isVerificationRequired = isVerificationRequired ;
this . _sendVerificationEmail = sendVerificationEmail ;
this . _Settings = Settings ;
this . _eventRepository = eventRepository ;
2022-02-03 18:02:33 +03:00
2022-10-06 08:50:27 +03:00
this . _handleMemberCreatedEvent = this . _handleMemberCreatedEvent . bind ( this ) ;
DomainEvents . subscribe ( MemberCreatedEvent , this . _handleMemberCreatedEvent ) ;
2022-08-24 09:06:05 +03:00
}
2022-02-03 18:02:33 +03:00
2023-01-04 13:22:12 +03:00
get _apiTriggerThreshold ( ) {
return this . _getApiTriggerThreshold ( ) ;
}
get _adminTriggerThreshold ( ) {
return this . _getAdminTriggerThreshold ( ) ;
}
get _importTriggerThreshold ( ) {
return this . _getImportTriggerThreshold ( ) ;
}
2022-08-25 09:26:26 +03:00
/ * *
2022-11-17 22:41:39 +03:00
*
* @ param { MemberCreatedEvent } event
2022-08-25 09:26:26 +03:00
* /
2022-10-06 08:50:27 +03:00
async _handleMemberCreatedEvent ( event ) {
2022-08-24 09:23:32 +03:00
const source = event . data ? . source ;
2022-08-25 09:26:26 +03:00
let sourceThreshold ;
if ( source === 'api' ) {
sourceThreshold = this . _apiTriggerThreshold ;
} else if ( source === 'admin' ) {
sourceThreshold = this . _adminTriggerThreshold ;
}
2022-08-24 09:23:32 +03:00
2022-08-25 09:26:26 +03:00
if ( [ 'api' , 'admin' ] . includes ( source ) && isFinite ( sourceThreshold ) ) {
2022-08-24 09:06:05 +03:00
const createdAt = new Date ( ) ;
createdAt . setDate ( createdAt . getDate ( ) - 30 ) ;
2022-11-18 17:05:15 +03:00
const events = await this . _eventRepository . getSignupEvents ( { } , {
2022-11-01 07:31:49 +03:00
source : source ,
created _at : {
$gt : createdAt . toISOString ( ) . replace ( 'T' , ' ' ) . substring ( 0 , 19 )
}
2022-08-24 09:06:05 +03:00
} ) ;
2023-03-21 21:00:14 +03:00
const membersTotal = ( await this . _eventRepository . getSignupEvents ( { } , {
source : 'member'
} ) ) . meta . pagination . total ;
if ( events . meta . pagination . total > Math . max ( sourceThreshold , membersTotal ) ) {
2022-08-25 04:47:59 +03:00
await this . _startVerificationProcess ( {
amount : events . meta . pagination . total ,
2022-08-24 09:06:05 +03:00
throwOnTrigger : false ,
2022-08-24 09:23:32 +03:00
source : source
2022-08-24 09:06:05 +03:00
} ) ;
2022-02-03 18:02:33 +03:00
}
2022-08-24 09:06:05 +03:00
}
2022-01-27 13:57:51 +03:00
}
async getImportThreshold ( ) {
2022-08-25 12:07:03 +03:00
const volumeThreshold = this . _importTriggerThreshold ;
2022-01-27 13:57:51 +03:00
if ( isFinite ( volumeThreshold ) ) {
2023-03-21 21:00:14 +03:00
const membersTotal = ( await this . _eventRepository . getSignupEvents ( { } , {
source : 'member'
} ) ) . meta . pagination . total ;
2022-01-27 13:57:51 +03:00
return Math . max ( membersTotal , volumeThreshold ) ;
} else {
return volumeThreshold ;
}
}
2023-01-04 13:22:12 +03:00
/ * *
* Returns false if email verification is required to send an email . It also updates the verification check and might activate email verification .
* Use this when sending emails .
* /
async checkVerificationRequired ( ) {
// Check if import threshold is reached (could happen that a long import is in progress and we didn't check the threshold yet)
await this . testImportThreshold ( ) ;
return this . _isVerificationRequired ( ) && ! this . _isVerified ( ) ;
}
2022-04-13 19:35:24 +03:00
async testImportThreshold ( ) {
2022-08-25 12:07:03 +03:00
if ( ! isFinite ( this . _importTriggerThreshold ) ) {
2022-07-26 12:35:48 +03:00
// Infinite threshold, quick path
return ;
}
2023-01-04 13:22:12 +03:00
if ( this . _isVerified ( ) ) {
// Already verified, no need to check limits
return ;
}
if ( this . _isVerificationRequired ( ) ) {
// Already requested verification, no need to calculate again
return ;
}
2022-04-13 19:35:24 +03:00
const createdAt = new Date ( ) ;
createdAt . setDate ( createdAt . getDate ( ) - 30 ) ;
2022-11-18 17:05:15 +03:00
const events = await this . _eventRepository . getSignupEvents ( { } , {
2022-11-01 07:31:49 +03:00
source : 'import' ,
created _at : {
$gt : createdAt . toISOString ( ) . replace ( 'T' , ' ' ) . substring ( 0 , 19 )
}
2022-04-13 19:35:24 +03:00
} ) ;
2023-03-21 21:00:14 +03:00
const membersTotal = ( await this . _eventRepository . getSignupEvents ( { } , {
source : 'member'
} ) ) . meta . pagination . total ;
2022-04-13 19:35:24 +03:00
// Import threshold is either the total number of members (discounting any created by imports in
// the last 30 days) or the threshold defined in config, whichever is greater.
2022-08-25 12:07:03 +03:00
const importThreshold = Math . max ( membersTotal - events . meta . pagination . total , this . _importTriggerThreshold ) ;
2022-04-13 19:35:24 +03:00
if ( isFinite ( importThreshold ) && events . meta . pagination . total > importThreshold ) {
2022-08-25 04:47:59 +03:00
await this . _startVerificationProcess ( {
amount : events . meta . pagination . total ,
2022-04-13 19:35:24 +03:00
throwOnTrigger : false ,
source : 'import'
} ) ;
}
}
2022-02-04 14:54:48 +03:00
/ * *
* @ typedef IVerificationResult
2022-01-27 13:57:51 +03:00
* @ property { boolean } needsVerification Whether the verification workflow was triggered
* /
/ * *
2022-02-04 14:54:48 +03:00
*
2022-01-27 13:57:51 +03:00
* @ param { object } config
2022-08-25 04:47:59 +03:00
* @ param { number } config . amount The amount of members that triggered the verification process
2022-01-27 13:57:51 +03:00
* @ param { boolean } config . throwOnTrigger Whether to throw if verification is needed
2022-02-03 18:03:47 +03:00
* @ param { string } config . source Source of the verification trigger - currently either 'api' or 'import'
2022-02-04 14:54:48 +03:00
* @ returns { Promise < IVerificationResult > } Object containing property "needsVerification" - true when triggered
2022-01-27 13:57:51 +03:00
* /
2022-08-25 04:47:59 +03:00
async _startVerificationProcess ( {
amount ,
2022-02-03 18:03:47 +03:00
throwOnTrigger ,
source
2022-01-27 13:57:51 +03:00
} ) {
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 } } ) ;
2022-08-25 09:26:26 +03:00
// Setting import as a default message
let verificationMessage = messages . emailVerificationEmailMessageImport ;
if ( source === 'api' ) {
verificationMessage = messages . emailVerificationEmailMessageAPI ;
} else if ( source === 'admin' ) {
verificationMessage = messages . emailVerificationEmailMessageAdmin ;
}
2022-11-17 22:41:39 +03:00
2022-11-18 17:05:15 +03:00
await this . _sendVerificationEmail ( {
2022-08-25 09:26:26 +03:00
message : verificationMessage ,
2022-01-27 13:57:51 +03:00
subject : messages . emailVerificationEmailSubject ,
2022-08-25 05:43:46 +03:00
amountTriggered : amount
2022-01-27 13:57:51 +03:00
} ) ;
if ( throwOnTrigger ) {
2022-11-17 22:41:39 +03:00
throw new errors . HostLimitError ( {
message : messages . emailVerificationNeeded ,
code : 'EMAIL_VERIFICATION_NEEDED'
2022-01-27 13:57:51 +03:00
} ) ;
}
return {
needsVerification : true
} ;
}
}
return {
needsVerification : false
} ;
}
}
2022-02-04 14:54:48 +03:00
module . exports = VerificationTrigger ;