mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 18:01:36 +03:00
01d0b2b304
ref https://linear.app/tryghost/issue/KTLO-1/members-spam-signups - Some customers are seeing many spammy signups ("hundreds a day") — our hypothesis is that bots and/or email link checkers are able to signup by simply following the link in the email without even loading the page in a browser. - Currently new members signup by clicking a magic link in an email, which is a simple GET request. When the user (or a bot) clicks that link, Ghost creates the member and signs them in for the first time. - This change, behind an alpha flag, requires a new member to click the link in the email, which takes them to a new frontend route `/confirm_signup/`, then submit a form on the page which sends a POST request to the server. If JavaScript is enabled, the form will be submitted automatically so the only change to the user is an extra flash/redirect before being signed in and redirected to the homepage. - This change is behind the alpha flag `membersSpamPrevention` so we can test it out on a few customer's sites and see if it helps reduce the spam signups. With the flag off, the signup flow remains the same as before.
296 lines
10 KiB
JavaScript
296 lines
10 KiB
JavaScript
const {agentProvider, mockManager, fixtureManager, matchers, resetRateLimits} = require('../../utils/e2e-framework');
|
|
const {mockLabsDisabled} = require('../../utils/e2e-framework-mock-manager');
|
|
const should = require('should');
|
|
const settingsCache = require('../../../core/shared/settings-cache');
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
const {anyErrorId} = matchers;
|
|
|
|
let membersAgent, membersService;
|
|
|
|
describe('sendMagicLink', function () {
|
|
before(async function () {
|
|
const agents = await agentProvider.getAgentsForMembers();
|
|
membersAgent = agents.membersAgent;
|
|
|
|
membersService = require('../../../core/server/services/members');
|
|
|
|
await fixtureManager.init('members');
|
|
});
|
|
|
|
beforeEach(function () {
|
|
mockManager.mockMail();
|
|
resetRateLimits();
|
|
// Reset settings
|
|
settingsCache.set('members_signup_access', {value: 'all'});
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockManager.restore();
|
|
});
|
|
|
|
it('Errors when passed multiple emails', async function () {
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email: 'one@test.com,two@test.com',
|
|
emailType: 'signup'
|
|
})
|
|
.expectStatus(400);
|
|
});
|
|
|
|
it('Throws an error when logging in to a email that does not exist', async function () {
|
|
const email = 'this-member-does-not-exist@test.com';
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email,
|
|
emailType: 'signin'
|
|
})
|
|
.expectStatus(400)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId,
|
|
// Add this here because it is easy to be overlooked (we need a human readable error!)
|
|
// 'Please sign up first' should be included only when invite only is disabled.
|
|
message: 'No member exists with this e-mail address. Please sign up first.'
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('Throws an error when logging in to a email that does not exist (invite only)', async function () {
|
|
settingsCache.set('members_signup_access', {value: 'invite'});
|
|
|
|
const email = 'this-member-does-not-exist@test.com';
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email,
|
|
emailType: 'signin'
|
|
})
|
|
.expectStatus(400)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId,
|
|
// Add this here because it is easy to be overlooked (we need a human readable error!)
|
|
// 'Please sign up first' should NOT be included
|
|
message: 'No member exists with this e-mail address.'
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('Throws an error when trying to sign up on an invite only site', async function () {
|
|
settingsCache.set('members_signup_access', {value: 'invite'});
|
|
|
|
const email = 'this-member-does-not-exist@test.com';
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email,
|
|
emailType: 'signup'
|
|
})
|
|
.expectStatus(400)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('Creates a valid magic link with tokenData, and without urlHistory', async function () {
|
|
const email = 'newly-created-user-magic-link-test@test.com';
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email,
|
|
emailType: 'signup'
|
|
})
|
|
.expectEmptyBody()
|
|
.expectStatus(201);
|
|
|
|
// Check email is sent
|
|
const mail = mockManager.assert.sentEmail({
|
|
to: email,
|
|
subject: /Complete your sign up to Ghost!/
|
|
});
|
|
|
|
// Get link from email
|
|
const [url] = mail.text.match(/https?:\/\/[^\s]+/);
|
|
const parsed = new URL(url);
|
|
const token = parsed.searchParams.get('token');
|
|
|
|
// Get data
|
|
const data = await membersService.api.getTokenDataFromMagicLinkToken(token);
|
|
|
|
should(data).match({
|
|
email,
|
|
attribution: {
|
|
id: null,
|
|
url: null,
|
|
type: null
|
|
}
|
|
});
|
|
});
|
|
|
|
it('Creates a valid magic link from custom signup with redirection', async function () {
|
|
const customSignupUrl = 'http://localhost:2368/custom-signup-form-page';
|
|
const email = 'newly-created-user-magic-link-test@test.com';
|
|
await membersAgent
|
|
.post('/api/send-magic-link')
|
|
.header('Referer', customSignupUrl)
|
|
.body({
|
|
email,
|
|
emailType: 'signup',
|
|
autoRedirect: true
|
|
})
|
|
.expectEmptyBody()
|
|
.expectStatus(201);
|
|
|
|
const mail = await mockManager.assert.sentEmail({
|
|
to: email,
|
|
subject: /Complete your sign up to Ghost!/
|
|
});
|
|
const [url] = mail.text.match(/https?:\/\/[^\s]+/);
|
|
const parsed = new URL(url);
|
|
const redirect = parsed.searchParams.get('r');
|
|
should(redirect).equal(customSignupUrl);
|
|
});
|
|
|
|
it('Creates a valid magic link from custom signup with redirection disabled', async function () {
|
|
const customSignupUrl = 'http://localhost:2368/custom-signup-form-page';
|
|
const email = 'newly-created-user-magic-link-test@test.com';
|
|
await membersAgent
|
|
.post('/api/send-magic-link')
|
|
.header('Referer', customSignupUrl)
|
|
.body({
|
|
email,
|
|
emailType: 'signup',
|
|
autoRedirect: false
|
|
})
|
|
.expectEmptyBody()
|
|
.expectStatus(201);
|
|
|
|
const mail = await mockManager.assert.sentEmail({
|
|
to: email,
|
|
subject: /Complete your sign up to Ghost!/
|
|
});
|
|
const [url] = mail.text.match(/https?:\/\/[^\s]+/);
|
|
const parsed = new URL(url);
|
|
const redirect = parsed.searchParams.get('r');
|
|
should(redirect).equal(null);
|
|
});
|
|
|
|
it('triggers email alert for free member signup', async function () {
|
|
mockLabsDisabled('membersSpamPrevention');
|
|
const email = 'newly-created-user-magic-link-test@test.com';
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email,
|
|
emailType: 'signup'
|
|
})
|
|
.expectEmptyBody()
|
|
.expectStatus(201);
|
|
|
|
// Check email is sent
|
|
const mail = mockManager.assert.sentEmail({
|
|
to: email,
|
|
subject: /Complete your sign up to Ghost!/
|
|
});
|
|
|
|
// Get link from email
|
|
const [url] = mail.text.match(/https?:\/\/[^\s]+/);
|
|
const parsed = new URL(url);
|
|
const token = parsed.searchParams.get('token');
|
|
|
|
// Get member data from token
|
|
const data = await membersService.api.getMemberDataFromMagicLinkToken(token);
|
|
|
|
// Wait for the dispatched events (because this happens async)
|
|
await DomainEvents.allSettled();
|
|
// Check member alert is sent to site owners
|
|
mockManager.assert.sentEmail({
|
|
to: 'jbloggs@example.com',
|
|
subject: /🥳 Free member signup: newly-created-user-magic-link-test@test.com/
|
|
});
|
|
|
|
// Check member data is returned
|
|
should(data).match({
|
|
email
|
|
});
|
|
});
|
|
|
|
it('triggers email alert for free member signup with membersSpamPrevention enabled', async function () {
|
|
const email = 'newly-created-user-magic-link-test-spam@test.com';
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email,
|
|
emailType: 'signup'
|
|
})
|
|
.expectEmptyBody()
|
|
.expectStatus(201);
|
|
|
|
// Check email is sent
|
|
const mail = mockManager.assert.sentEmail({
|
|
to: email,
|
|
subject: /Complete your sign up to Ghost!/
|
|
});
|
|
|
|
// Get link from email
|
|
const [url] = mail.text.match(/https?:\/\/[^\s]+/);
|
|
const parsed = new URL(url);
|
|
const token = parsed.searchParams.get('token');
|
|
|
|
// Get member data from token
|
|
const signinLink = await membersService.api.createMemberFromToken(token);
|
|
|
|
// Wait for the dispatched events (because this happens async)
|
|
await DomainEvents.allSettled();
|
|
// Check member alert is sent to site owners
|
|
mockManager.assert.sentEmail({
|
|
to: 'jbloggs@example.com',
|
|
subject: /🥳 Free member signup: newly-created-user-magic-link-test-spam@test.com/
|
|
});
|
|
|
|
// Check the signin link is returned correctly
|
|
const parsedSigninLink = new URL(signinLink);
|
|
const signinToken = parsedSigninLink.searchParams.get('token');
|
|
const action = parsedSigninLink.searchParams.get('action');
|
|
should(action).equal('signin');
|
|
should(signinToken.length).equal(32);
|
|
});
|
|
|
|
it('Converts the urlHistory to the attribution and stores it in the token', async function () {
|
|
const email = 'newly-created-user-magic-link-test-10@test.com';
|
|
await membersAgent.post('/api/send-magic-link')
|
|
.body({
|
|
email,
|
|
emailType: 'signup',
|
|
urlHistory: [
|
|
{
|
|
path: '/test-path',
|
|
time: Date.now()
|
|
}
|
|
]
|
|
})
|
|
.expectEmptyBody()
|
|
.expectStatus(201);
|
|
|
|
// Check email is sent
|
|
const mail = mockManager.assert.sentEmail({
|
|
to: email,
|
|
subject: /Complete your sign up to Ghost!/
|
|
});
|
|
|
|
// Get link from email
|
|
const [url] = mail.text.match(/https?:\/\/[^\s]+/);
|
|
const parsed = new URL(url);
|
|
const token = parsed.searchParams.get('token');
|
|
|
|
// Get data
|
|
const data = await membersService.api.getTokenDataFromMagicLinkToken(token);
|
|
|
|
should(data).match({
|
|
email,
|
|
attribution: {
|
|
id: null,
|
|
url: '/test-path',
|
|
type: 'url'
|
|
}
|
|
});
|
|
});
|
|
});
|