mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Added email verification trigger for admin requests
refs https://github.com/TryGhost/Toolbox/issues/387 - When members are added through the Admin client they have to be a part of instance validation process to prevent service misuse.
This commit is contained in:
parent
7cae68baaa
commit
7e3b8ff404
3 changed files with 123 additions and 6 deletions
|
@ -106,6 +106,7 @@ module.exports = {
|
||||||
|
|
||||||
verificationTrigger = new VerificationTrigger({
|
verificationTrigger = new VerificationTrigger({
|
||||||
apiTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
|
apiTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
|
||||||
|
adminTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.adminThreshold'),
|
||||||
isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
|
isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
|
||||||
isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
|
isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
|
||||||
sendVerificationEmail: ({subject, message, amountTriggered}) => {
|
sendVerificationEmail: ({subject, message, amountTriggered}) => {
|
||||||
|
@ -189,4 +190,5 @@ module.exports = {
|
||||||
stats: membersStats,
|
stats: membersStats,
|
||||||
export: require('./exporter/query')
|
export: require('./exporter/query')
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.middleware = require('./middleware');
|
module.exports.middleware = require('./middleware');
|
||||||
|
|
|
@ -6,6 +6,7 @@ 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.`,
|
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`,
|
emailVerificationEmailSubject: `Email needs verification`,
|
||||||
emailVerificationEmailMessageImport: `Email verification needed for site: {siteUrl}, has imported: {amountTriggered} members in the last 30 days.`,
|
emailVerificationEmailMessageImport: `Email verification needed for site: {siteUrl}, has imported: {amountTriggered} members in the last 30 days.`,
|
||||||
|
emailVerificationEmailMessageAdmin: `Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the Admin client in the last 30 days.`,
|
||||||
emailVerificationEmailMessageAPI: `Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the API in the last 30 days.`
|
emailVerificationEmailMessageAPI: `Email verification needed for site: {siteUrl} has added: {amountTriggered} members through the API in the last 30 days.`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,7 +14,8 @@ class VerificationTrigger {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {object} deps
|
* @param {object} deps
|
||||||
* @param {number} deps.apiTriggerThreshold Threshold for triggering verification as defined in config
|
* @param {number} deps.apiTriggerThreshold Threshold for triggering API&Import sourced verifications
|
||||||
|
* @param {number} deps.adminTriggerThreshold Threshold for triggering Admin sourced verifications
|
||||||
* @param {() => boolean} deps.isVerified Check Ghost config to see if we are already verified
|
* @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 {() => boolean} deps.isVerificationRequired Check Ghost settings to see whether verification has been requested
|
||||||
* @param {(content: {subject: string, message: string, amountTriggered: number}) => void} deps.sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
|
* @param {(content: {subject: string, message: string, amountTriggered: number}) => void} deps.sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
|
||||||
|
@ -23,6 +25,7 @@ class VerificationTrigger {
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
apiTriggerThreshold,
|
apiTriggerThreshold,
|
||||||
|
adminTriggerThreshold,
|
||||||
isVerified,
|
isVerified,
|
||||||
isVerificationRequired,
|
isVerificationRequired,
|
||||||
sendVerificationEmail,
|
sendVerificationEmail,
|
||||||
|
@ -31,6 +34,7 @@ class VerificationTrigger {
|
||||||
eventRepository
|
eventRepository
|
||||||
}) {
|
}) {
|
||||||
this._apiTriggerThreshold = apiTriggerThreshold;
|
this._apiTriggerThreshold = apiTriggerThreshold;
|
||||||
|
this._adminTriggerThreshold = adminTriggerThreshold;
|
||||||
this._isVerified = isVerified;
|
this._isVerified = isVerified;
|
||||||
this._isVerificationRequired = isVerificationRequired;
|
this._isVerificationRequired = isVerificationRequired;
|
||||||
this._sendVerificationEmail = sendVerificationEmail;
|
this._sendVerificationEmail = sendVerificationEmail;
|
||||||
|
@ -42,11 +46,21 @@ class VerificationTrigger {
|
||||||
DomainEvents.subscribe(MemberSubscribeEvent, this._handleMemberSubscribeEvent);
|
DomainEvents.subscribe(MemberSubscribeEvent, this._handleMemberSubscribeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {MemberSubscribeEvent} event
|
||||||
|
*/
|
||||||
async _handleMemberSubscribeEvent(event) {
|
async _handleMemberSubscribeEvent(event) {
|
||||||
const source = event.data?.source;
|
const source = event.data?.source;
|
||||||
const sourceThreshold = this._apiTriggerThreshold;
|
let sourceThreshold;
|
||||||
|
|
||||||
if (source === 'api' && isFinite(sourceThreshold)) {
|
if (source === 'api') {
|
||||||
|
sourceThreshold = this._apiTriggerThreshold;
|
||||||
|
} else if (source === 'admin') {
|
||||||
|
sourceThreshold = this._adminTriggerThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['api', 'admin'].includes(source) && isFinite(sourceThreshold)) {
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
createdAt.setDate(createdAt.getDate() - 30);
|
createdAt.setDate(createdAt.getDate() - 30);
|
||||||
const events = await this._eventRepository.getNewsletterSubscriptionEvents({}, {
|
const events = await this._eventRepository.getNewsletterSubscriptionEvents({}, {
|
||||||
|
@ -127,10 +141,17 @@ class VerificationTrigger {
|
||||||
value: true
|
value: true
|
||||||
}], {context: {internal: true}});
|
}], {context: {internal: true}});
|
||||||
|
|
||||||
|
// Setting import as a default message
|
||||||
|
let verificationMessage = messages.emailVerificationEmailMessageImport;
|
||||||
|
|
||||||
|
if (source === 'api') {
|
||||||
|
verificationMessage = messages.emailVerificationEmailMessageAPI;
|
||||||
|
} else if (source === 'admin') {
|
||||||
|
verificationMessage = messages.emailVerificationEmailMessageAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
this._sendVerificationEmail({
|
this._sendVerificationEmail({
|
||||||
message: source === 'api'
|
message: verificationMessage,
|
||||||
? messages.emailVerificationEmailMessageAPI
|
|
||||||
: messages.emailVerificationEmailMessageImport,
|
|
||||||
subject: messages.emailVerificationEmailSubject,
|
subject: messages.emailVerificationEmailSubject,
|
||||||
amountTriggered: amount
|
amountTriggered: amount
|
||||||
});
|
});
|
||||||
|
|
|
@ -233,6 +233,100 @@ describe('Email verification flow', function () {
|
||||||
amountTriggered: 10
|
amountTriggered: 10
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Triggers when a number of members are added from Admin', async function () {
|
||||||
|
const emailStub = sinon.stub().resolves(null);
|
||||||
|
const settingsStub = sinon.stub().resolves(null);
|
||||||
|
const eventStub = sinon.stub().resolves({
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
total: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const trigger = new VerificationTrigger({
|
||||||
|
adminTriggerThreshold: 2,
|
||||||
|
Settings: {
|
||||||
|
edit: settingsStub
|
||||||
|
},
|
||||||
|
membersStats: {
|
||||||
|
getTotalMembers: () => 15
|
||||||
|
},
|
||||||
|
isVerified: () => false,
|
||||||
|
isVerificationRequired: () => false,
|
||||||
|
sendVerificationEmail: emailStub,
|
||||||
|
eventRepository: {
|
||||||
|
getNewsletterSubscriptionEvents: eventStub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await trigger._handleMemberSubscribeEvent({
|
||||||
|
data: {
|
||||||
|
source: 'admin'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventStub.callCount.should.eql(1);
|
||||||
|
eventStub.lastCall.lastArg.should.have.property('data.source');
|
||||||
|
eventStub.lastCall.lastArg.should.have.property('data.created_at');
|
||||||
|
eventStub.lastCall.lastArg['data.source'].should.eql(`data.source:'admin'`);
|
||||||
|
eventStub.lastCall.lastArg['data.created_at'].should.startWith(`data.created_at:>'`);
|
||||||
|
|
||||||
|
emailStub.callCount.should.eql(1);
|
||||||
|
emailStub.lastCall.firstArg.should.eql({
|
||||||
|
subject: 'Email needs verification',
|
||||||
|
message: 'Email verification needed for site: {siteUrl} has added: {importedNumber} members through the Admin client in the last 30 days.',
|
||||||
|
amountTriggered: 10
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Triggers when a number of members are added from API', async function () {
|
||||||
|
const emailStub = sinon.stub().resolves(null);
|
||||||
|
const settingsStub = sinon.stub().resolves(null);
|
||||||
|
const eventStub = sinon.stub().resolves({
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
total: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const trigger = new VerificationTrigger({
|
||||||
|
adminTriggerThreshold: 2,
|
||||||
|
apiTriggerThreshold: 2,
|
||||||
|
Settings: {
|
||||||
|
edit: settingsStub
|
||||||
|
},
|
||||||
|
membersStats: {
|
||||||
|
getTotalMembers: () => 15
|
||||||
|
},
|
||||||
|
isVerified: () => false,
|
||||||
|
isVerificationRequired: () => false,
|
||||||
|
sendVerificationEmail: emailStub,
|
||||||
|
eventRepository: {
|
||||||
|
getNewsletterSubscriptionEvents: eventStub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await trigger._handleMemberSubscribeEvent({
|
||||||
|
data: {
|
||||||
|
source: 'api'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventStub.callCount.should.eql(1);
|
||||||
|
eventStub.lastCall.lastArg.should.have.property('data.source');
|
||||||
|
eventStub.lastCall.lastArg.should.have.property('data.created_at');
|
||||||
|
eventStub.lastCall.lastArg['data.source'].should.eql(`data.source:'admin'`);
|
||||||
|
eventStub.lastCall.lastArg['data.created_at'].should.startWith(`data.created_at:>'`);
|
||||||
|
|
||||||
|
emailStub.callCount.should.eql(1);
|
||||||
|
emailStub.lastCall.firstArg.should.eql({
|
||||||
|
subject: 'Email needs verification',
|
||||||
|
message: 'Email verification needed for site: {siteUrl} has added: {importedNumber} members through the API in the last 30 days.',
|
||||||
|
amountTriggered: 10
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Does not fetch events and trigger when threshold is Infinity', async function () {
|
it('Does not fetch events and trigger when threshold is Infinity', async function () {
|
||||||
|
|
Loading…
Add table
Reference in a new issue