Added support in Portal for integrity tokens on magic link API
ref KTLO-1 These tokens should prevent untargeted attacks, as the magic link endpoint needs a token that was generated by the server, similar to a CSRF token, but without needing any server-side state, or a cookie to be set for unauthenticated users.
This commit is contained in:
parent
a48b4e5cbf
commit
ef4f79370f
@ -79,7 +79,8 @@ async function signout({api, state}) {
|
|||||||
|
|
||||||
async function signin({data, api, state}) {
|
async function signin({data, api, state}) {
|
||||||
try {
|
try {
|
||||||
await api.member.sendMagicLink({...data, emailType: 'signin'});
|
const integrityToken = await api.member.getIntegrityToken();
|
||||||
|
await api.member.sendMagicLink({...data, emailType: 'signin', integrityToken});
|
||||||
return {
|
return {
|
||||||
page: 'magiclink',
|
page: 'magiclink',
|
||||||
lastPage: 'signin'
|
lastPage: 'signin'
|
||||||
@ -100,7 +101,8 @@ async function signup({data, state, api}) {
|
|||||||
let {plan, tierId, cadence, email, name, newsletters, offerId} = data;
|
let {plan, tierId, cadence, email, name, newsletters, offerId} = data;
|
||||||
|
|
||||||
if (plan.toLowerCase() === 'free') {
|
if (plan.toLowerCase() === 'free') {
|
||||||
await api.member.sendMagicLink({emailType: 'signup', ...data});
|
const integrityToken = await api.member.getIntegrityToken();
|
||||||
|
await api.member.sendMagicLink({emailType: 'signup', integrityToken, ...data});
|
||||||
} else {
|
} else {
|
||||||
if (tierId && cadence) {
|
if (tierId && cadence) {
|
||||||
await api.member.checkoutPlan({plan, tierId, cadence, email, name, newsletters, offerId});
|
await api.member.checkoutPlan({plan, tierId, cadence, email, name, newsletters, offerId});
|
||||||
|
@ -56,12 +56,21 @@ export function formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`${siteUrl}/members/api/send-magic-link/`, {
|
return fetch(`${siteUrl}/members/api/integrity-token/`, {
|
||||||
method: 'POST',
|
method: 'GET'
|
||||||
headers: {
|
}).then((res) => {
|
||||||
'Content-Type': 'application/json'
|
return res.text();
|
||||||
},
|
}).then((integrityToken) => {
|
||||||
body: JSON.stringify(reqBody)
|
return fetch(`${siteUrl}/members/api/send-magic-link/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...reqBody,
|
||||||
|
integrityToken
|
||||||
|
})
|
||||||
|
});
|
||||||
}).then(function (res) {
|
}).then(function (res) {
|
||||||
form.addEventListener('submit', submitHandler);
|
form.addEventListener('submit', submitHandler);
|
||||||
form.classList.remove('loading');
|
form.classList.remove('loading');
|
||||||
|
@ -16,6 +16,10 @@ const setup = async ({site, member = null}) => {
|
|||||||
return Promise.resolve('success');
|
return Promise.resolve('success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ghostApi.member.getIntegrityToken = jest.fn(() => {
|
||||||
|
return Promise.resolve('testtoken');
|
||||||
|
});
|
||||||
|
|
||||||
ghostApi.member.checkoutPlan = jest.fn(() => {
|
ghostApi.member.checkoutPlan = jest.fn(() => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
@ -67,6 +71,10 @@ const multiTierSetup = async ({site, member = null}) => {
|
|||||||
return Promise.resolve('success');
|
return Promise.resolve('success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ghostApi.member.getIntegrityToken = jest.fn(() => {
|
||||||
|
return Promise.resolve(`testtoken`);
|
||||||
|
});
|
||||||
|
|
||||||
ghostApi.member.checkoutPlan = jest.fn(() => {
|
ghostApi.member.checkoutPlan = jest.fn(() => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
@ -139,13 +147,15 @@ describe('Signin', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
|
||||||
email: 'jamie@example.com',
|
|
||||||
emailType: 'signin'
|
|
||||||
});
|
|
||||||
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
||||||
expect(magicLink).toBeInTheDocument();
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
emailType: 'signin',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('without name field', async () => {
|
test('without name field', async () => {
|
||||||
@ -165,13 +175,15 @@ describe('Signin', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
|
||||||
email: 'jamie@example.com',
|
|
||||||
emailType: 'signin'
|
|
||||||
});
|
|
||||||
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
||||||
expect(magicLink).toBeInTheDocument();
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
emailType: 'signin',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('with only free plan', async () => {
|
test('with only free plan', async () => {
|
||||||
@ -191,13 +203,15 @@ describe('Signin', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
|
||||||
email: 'jamie@example.com',
|
|
||||||
emailType: 'signin'
|
|
||||||
});
|
|
||||||
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
||||||
expect(magicLink).toBeInTheDocument();
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
emailType: 'signin',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -231,13 +245,15 @@ describe('Signin', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
|
||||||
email: 'jamie@example.com',
|
|
||||||
emailType: 'signin'
|
|
||||||
});
|
|
||||||
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
||||||
expect(magicLink).toBeInTheDocument();
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
emailType: 'signin',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('without name field', async () => {
|
test('without name field', async () => {
|
||||||
@ -257,13 +273,15 @@ describe('Signin', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
|
||||||
email: 'jamie@example.com',
|
|
||||||
emailType: 'signin'
|
|
||||||
});
|
|
||||||
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
||||||
expect(magicLink).toBeInTheDocument();
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
emailType: 'signin',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('with only free plan available', async () => {
|
test('with only free plan available', async () => {
|
||||||
@ -283,13 +301,15 @@ describe('Signin', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
|
||||||
email: 'jamie@example.com',
|
|
||||||
emailType: 'signin'
|
|
||||||
});
|
|
||||||
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i);
|
||||||
expect(magicLink).toBeInTheDocument();
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
emailType: 'signin',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,6 +16,10 @@ const offerSetup = async ({site, member = null, offer}) => {
|
|||||||
return Promise.resolve('success');
|
return Promise.resolve('success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ghostApi.member.getIntegrityToken = jest.fn(() => {
|
||||||
|
return Promise.resolve(`testtoken`);
|
||||||
|
});
|
||||||
|
|
||||||
ghostApi.site.offer = jest.fn(() => {
|
ghostApi.site.offer = jest.fn(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
offers: [offer]
|
offers: [offer]
|
||||||
@ -80,6 +84,10 @@ const setup = async ({site, member = null}) => {
|
|||||||
return Promise.resolve('success');
|
return Promise.resolve('success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ghostApi.member.getIntegrityToken = jest.fn(() => {
|
||||||
|
return Promise.resolve(`testtoken`);
|
||||||
|
});
|
||||||
|
|
||||||
ghostApi.member.checkoutPlan = jest.fn(() => {
|
ghostApi.member.checkoutPlan = jest.fn(() => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
@ -133,6 +141,10 @@ const multiTierSetup = async ({site, member = null}) => {
|
|||||||
return Promise.resolve('success');
|
return Promise.resolve('success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ghostApi.member.getIntegrityToken = jest.fn(() => {
|
||||||
|
return Promise.resolve(`testtoken`);
|
||||||
|
});
|
||||||
|
|
||||||
ghostApi.member.checkoutPlan = jest.fn(() => {
|
ghostApi.member.checkoutPlan = jest.fn(() => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
@ -205,14 +217,17 @@ describe('Signup', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
expect(nameInput).toHaveValue('Jamie Larsen');
|
expect(nameInput).toHaveValue('Jamie Larsen');
|
||||||
fireEvent.click(chooseBtns[0]);
|
fireEvent.click(chooseBtns[0]);
|
||||||
|
|
||||||
|
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
||||||
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
name: 'Jamie Larsen',
|
name: 'Jamie Larsen',
|
||||||
plan: 'free'
|
plan: 'free',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
|
||||||
expect(magicLink).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('without name field', async () => {
|
test('without name field', async () => {
|
||||||
@ -240,16 +255,17 @@ describe('Signup', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
fireEvent.click(chooseBtns[0]);
|
fireEvent.click(chooseBtns[0]);
|
||||||
|
|
||||||
|
// Check if magic link page is shown
|
||||||
|
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
||||||
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
name: '',
|
name: '',
|
||||||
plan: 'free'
|
plan: 'free',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if magic link page is shown
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
|
||||||
expect(magicLink).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('with only free plan', async () => {
|
test('with only free plan', async () => {
|
||||||
@ -288,16 +304,17 @@ describe('Signup', () => {
|
|||||||
expect(nameInput).toHaveValue('Jamie Larsen');
|
expect(nameInput).toHaveValue('Jamie Larsen');
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
// Check if magic link page is shown
|
||||||
|
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
||||||
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
name: 'Jamie Larsen',
|
name: 'Jamie Larsen',
|
||||||
plan: 'free'
|
plan: 'free',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if magic link page is shown
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
|
||||||
expect(magicLink).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -570,14 +587,17 @@ describe('Signup', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
expect(nameInput).toHaveValue('Jamie Larsen');
|
expect(nameInput).toHaveValue('Jamie Larsen');
|
||||||
fireEvent.click(chooseBtns[0]);
|
fireEvent.click(chooseBtns[0]);
|
||||||
|
|
||||||
|
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
||||||
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
name: 'Jamie Larsen',
|
name: 'Jamie Larsen',
|
||||||
plan: 'free'
|
plan: 'free',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
|
||||||
expect(magicLink).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('without name field', async () => {
|
test('without name field', async () => {
|
||||||
@ -601,16 +621,17 @@ describe('Signup', () => {
|
|||||||
expect(emailInput).toHaveValue('jamie@example.com');
|
expect(emailInput).toHaveValue('jamie@example.com');
|
||||||
fireEvent.click(chooseBtns[0]);
|
fireEvent.click(chooseBtns[0]);
|
||||||
|
|
||||||
|
// Check if magic link page is shown
|
||||||
|
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
||||||
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
name: '',
|
name: '',
|
||||||
plan: 'free'
|
plan: 'free',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if magic link page is shown
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
|
||||||
expect(magicLink).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('with only free plan available', async () => {
|
test('with only free plan available', async () => {
|
||||||
@ -646,16 +667,17 @@ describe('Signup', () => {
|
|||||||
expect(nameInput).toHaveValue('Jamie Larsen');
|
expect(nameInput).toHaveValue('Jamie Larsen');
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
// Check if magic link page is shown
|
||||||
|
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
||||||
|
expect(magicLink).toBeInTheDocument();
|
||||||
|
|
||||||
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
name: 'Jamie Larsen',
|
name: 'Jamie Larsen',
|
||||||
plan: 'free'
|
plan: 'free',
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if magic link page is shown
|
|
||||||
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
|
|
||||||
expect(magicLink).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not show free plan if it is hidden', async () => {
|
test('should not show free plan if it is hidden', async () => {
|
||||||
@ -799,4 +821,3 @@ describe('Signup', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -86,6 +86,13 @@ describe('Member Data attributes:', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.includes('api/integrity-token')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'testtoken'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (url.includes('api/session')) {
|
if (url.includes('api/session')) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -139,12 +146,12 @@ describe('Member Data attributes:', () => {
|
|||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
describe('data-members-form', () => {
|
describe('data-members-form', () => {
|
||||||
test('allows free signup', () => {
|
test('allows free signup', async () => {
|
||||||
const {event, form, errorEl, siteUrl, submitHandler} = getMockData();
|
const {event, form, errorEl, siteUrl, submitHandler} = getMockData();
|
||||||
|
|
||||||
formSubmitHandler({event, form, errorEl, siteUrl, submitHandler});
|
await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler});
|
||||||
|
|
||||||
expect(window.fetch).toHaveBeenCalledTimes(1);
|
expect(window.fetch).toHaveBeenCalledTimes(2);
|
||||||
const expectedBody = JSON.stringify({
|
const expectedBody = JSON.stringify({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
@ -157,9 +164,10 @@ describe('Member Data attributes:', () => {
|
|||||||
refSource: 'ghost-explore',
|
refSource: 'ghost-explore',
|
||||||
refUrl: 'https://example.com/blog/',
|
refUrl: 'https://example.com/blog/',
|
||||||
time: 1611234567890
|
time: 1611234567890
|
||||||
}]
|
}],
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
expect(window.fetch).toHaveBeenCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
|
expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -241,16 +249,16 @@ describe('Member Data attributes:', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('data-members-newsletter', () => {
|
describe('data-members-newsletter', () => {
|
||||||
test('includes specified newsletters in request', () => {
|
test('includes specified newsletters in request', async () => {
|
||||||
const {event, form, errorEl, siteUrl, submitHandler} = getMockData({
|
const {event, form, errorEl, siteUrl, submitHandler} = getMockData({
|
||||||
newsletterQuerySelectorResult: [{
|
newsletterQuerySelectorResult: [{
|
||||||
value: 'Some Newsletter'
|
value: 'Some Newsletter'
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
formSubmitHandler({event, form, errorEl, siteUrl, submitHandler});
|
await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler});
|
||||||
|
|
||||||
expect(window.fetch).toHaveBeenCalledTimes(1);
|
expect(window.fetch).toHaveBeenCalledTimes(2);
|
||||||
const expectedBody = JSON.stringify({
|
const expectedBody = JSON.stringify({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
@ -264,19 +272,20 @@ describe('Member Data attributes:', () => {
|
|||||||
refUrl: 'https://example.com/blog/',
|
refUrl: 'https://example.com/blog/',
|
||||||
time: 1611234567890
|
time: 1611234567890
|
||||||
}],
|
}],
|
||||||
newsletters: [{name: 'Some Newsletter'}]
|
newsletters: [{name: 'Some Newsletter'}],
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
expect(window.fetch).toHaveBeenCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
|
expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not include newsletters in request if there are no newsletter inputs', () => {
|
test('does not include newsletters in request if there are no newsletter inputs', async () => {
|
||||||
const {event, form, errorEl, siteUrl, submitHandler} = getMockData({
|
const {event, form, errorEl, siteUrl, submitHandler} = getMockData({
|
||||||
newsletterQuerySelectorResult: []
|
newsletterQuerySelectorResult: []
|
||||||
});
|
});
|
||||||
|
|
||||||
formSubmitHandler({event, form, errorEl, siteUrl, submitHandler});
|
await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler});
|
||||||
|
|
||||||
expect(window.fetch).toHaveBeenCalledTimes(1);
|
expect(window.fetch).toHaveBeenCalledTimes(2);
|
||||||
const expectedBody = JSON.stringify({
|
const expectedBody = JSON.stringify({
|
||||||
email: 'jamie@example.com',
|
email: 'jamie@example.com',
|
||||||
emailType: 'signup',
|
emailType: 'signup',
|
||||||
@ -289,9 +298,10 @@ describe('Member Data attributes:', () => {
|
|||||||
refSource: 'ghost-explore',
|
refSource: 'ghost-explore',
|
||||||
refUrl: 'https://example.com/blog/',
|
refUrl: 'https://example.com/blog/',
|
||||||
time: 1611234567890
|
time: 1611234567890
|
||||||
}]
|
}],
|
||||||
|
integrityToken: 'testtoken'
|
||||||
});
|
});
|
||||||
expect(window.fetch).toHaveBeenCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
|
expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -244,7 +244,21 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters, redirect, customUrlHistory, autoRedirect = true}) {
|
async getIntegrityToken() {
|
||||||
|
const url = endpointFor({type: 'members', resource: 'integrity-token'});
|
||||||
|
const res = await makeRequest({
|
||||||
|
url,
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return res.text();
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to start a members session');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters, redirect, integrityToken, customUrlHistory, autoRedirect = true}) {
|
||||||
const url = endpointFor({type: 'members', resource: 'send-magic-link'});
|
const url = endpointFor({type: 'members', resource: 'send-magic-link'});
|
||||||
const body = {
|
const body = {
|
||||||
name,
|
name,
|
||||||
@ -255,6 +269,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
|
|||||||
labels,
|
labels,
|
||||||
requestSrc: 'portal',
|
requestSrc: 'portal',
|
||||||
redirect,
|
redirect,
|
||||||
|
integrityToken,
|
||||||
autoRedirect
|
autoRedirect
|
||||||
};
|
};
|
||||||
const urlHistory = customUrlHistory ?? getUrlHistory();
|
const urlHistory = customUrlHistory ?? getUrlHistory();
|
||||||
|
@ -170,7 +170,7 @@ const createIntegrityToken = async function createIntegrityToken(req, res) {
|
|||||||
|
|
||||||
const verifyIntegrityToken = async function verifyIntegrityToken(req, res, next) {
|
const verifyIntegrityToken = async function verifyIntegrityToken(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const token = req.query.requestIntegrityToken;
|
const token = req.body.integrityToken;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
logging.warn('Request with missing integrity token.');
|
logging.warn('Request with missing integrity token.');
|
||||||
// In future this will throw an error
|
// In future this will throw an error
|
||||||
|
Loading…
Reference in New Issue
Block a user