diff --git a/ghost/core/core/server/services/members/middleware.js b/ghost/core/core/server/services/members/middleware.js index 441a9deb7e..73f1e5c79d 100644 --- a/ghost/core/core/server/services/members/middleware.js +++ b/ghost/core/core/server/services/members/middleware.js @@ -1,6 +1,7 @@ const _ = require('lodash'); const logging = require('@tryghost/logging'); const membersService = require('./service'); +const emailSuppressionList = require('../email-suppression-list'); const models = require('../../models'); const urlUtils = require('../../../shared/url-utils'); const spamPrevention = require('../../web/shared/middleware/api/spam-prevention'); @@ -39,7 +40,7 @@ const authMemberByUuid = async function (req, res, next) { // Already authenticated via session return next(); } - + throw new errors.UnauthorizedError({ messsage: tpl(messages.missingUuid) }); @@ -97,6 +98,20 @@ const getMemberData = async function (req, res) { } }; +const deleteSuppression = async function (req, res) { + try { + const member = await membersService.ssr.getMemberDataFromSession(req, res); + await emailSuppressionList.removeEmail(member.email); + res.writeHead(204); + res.end(); + } catch (err) { + res.writeHead(err.statusCode, { + 'Content-Type': 'text/plain;charset=UTF-8' + }); + res.end(err.message); + } +}; + const getMemberNewsletters = async function (req, res) { try { const memberUuid = req.query.uuid; @@ -262,5 +277,6 @@ module.exports = { getMemberData, updateMemberData, updateMemberNewsletters, - deleteSession + deleteSession, + deleteSuppression }; diff --git a/ghost/core/core/server/services/members/utils.js b/ghost/core/core/server/services/members/utils.js index b0f7253fcc..eee5eb68b3 100644 --- a/ghost/core/core/server/services/members/utils.js +++ b/ghost/core/core/server/services/members/utils.js @@ -1,3 +1,5 @@ +const labsService = require('../../../shared/labs'); + function formatNewsletterResponse(newsletters) { return newsletters.map(({id, name, description, sort_order: sortOrder}) => { return {id, name, description, sort_order: sortOrder}; @@ -23,5 +25,10 @@ module.exports.formattedMemberResponse = function formattedMemberResponse(member if (member.newsletters) { data.newsletters = formatNewsletterResponse(member.newsletters); } + + if (labsService.isSet('suppressionList') && member.email_suppression) { + data.email_suppression = member.email_suppression; + } + return data; }; diff --git a/ghost/core/core/server/web/members/app.js b/ghost/core/core/server/web/members/app.js index 1d20ad1fc7..8fdb483c0c 100644 --- a/ghost/core/core/server/web/members/app.js +++ b/ghost/core/core/server/web/members/app.js @@ -45,18 +45,21 @@ module.exports = function setupMembersApp() { membersApp.put('/api/member', bodyParser.json({limit: '50mb'}), middleware.updateMemberData); membersApp.post('/api/member/email', bodyParser.json({limit: '50mb'}), (req, res) => membersService.api.middleware.updateEmailAddress(req, res)); + // Remove email from suppression list + membersApp.delete('/api/member/suppression', labs.enabledMiddleware('suppressionList'), middleware.deleteSuppression); + // Manage session membersApp.get('/api/session', middleware.getIdentityToken); membersApp.delete('/api/session', middleware.deleteSession); // NOTE: this is wrapped in a function to ensure we always go via the getter membersApp.post( - '/api/send-magic-link', - bodyParser.json(), + '/api/send-magic-link', + bodyParser.json(), // Prevent brute forcing email addresses (user enumeration) - shared.middleware.brute.membersAuthEnumeration, + shared.middleware.brute.membersAuthEnumeration, // Prevent brute forcing passwords for the same email address - shared.middleware.brute.membersAuth, + shared.middleware.brute.membersAuth, (req, res, next) => membersService.api.middleware.sendMagicLink(req, res, next) ); membersApp.post('/api/create-stripe-checkout-session', (req, res, next) => membersService.api.middleware.createCheckoutSession(req, res, next)); @@ -68,11 +71,11 @@ module.exports = function setupMembersApp() { // Feedback membersApp.post( - '/api/feedback', - labs.enabledMiddleware('audienceFeedback'), - bodyParser.json({limit: '50mb'}), - middleware.loadMemberSession, - middleware.authMemberByUuid, + '/api/feedback', + labs.enabledMiddleware('audienceFeedback'), + bodyParser.json({limit: '50mb'}), + middleware.loadMemberSession, + middleware.authMemberByUuid, http(api.feedbackMembers.add) ); diff --git a/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap index e5b28432ac..1263f71db6 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap @@ -4,6 +4,10 @@ exports[`Comments API when authenticated can get member data 1: [body] 1`] = ` Object { "avatar_image": null, "email": "member@example.com", + "email_suppression": Object { + "info": null, + "suppressed": false, + }, "enable_comment_notifications": true, "expertise": null, "firstname": null, @@ -33,7 +37,7 @@ exports[`Comments API when authenticated can get member data 2: [headers] 1`] = Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "436", + "content-length": "489", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", diff --git a/ghost/core/test/e2e-api/members/middleware.test.js b/ghost/core/test/e2e-api/members/middleware.test.js index 631fdaa092..d4b543173c 100644 --- a/ghost/core/test/e2e-api/members/middleware.test.js +++ b/ghost/core/test/e2e-api/members/middleware.test.js @@ -196,5 +196,12 @@ describe('Comments API', function () { member = await models.Member.findOne({id: member.id}, {require: true}); member.get('enable_comment_notifications').should.eql(true); }); + + it('can remove member from suppression list', async function () { + await membersAgent + .delete(`/api/member/suppression`) + .expectStatus(204) + .expectEmptyBody(); + }); }); }); diff --git a/ghost/core/test/unit/server/services/members/utils.test.js b/ghost/core/test/unit/server/services/members/utils.test.js index 7f38bcb813..45c4afe4b9 100644 --- a/ghost/core/test/unit/server/services/members/utils.test.js +++ b/ghost/core/test/unit/server/services/members/utils.test.js @@ -24,7 +24,11 @@ describe('Members Service - utils', function () { subscribed: true, status: 'free', extra: 'property', - enable_comment_notifications: true + enable_comment_notifications: true, + email_suppression: { + suppressed: false, + info: null + } }); should(member1).deepEqual({ uuid: 'uuid-1', @@ -36,7 +40,11 @@ describe('Members Service - utils', function () { subscribed: true, subscriptions: [], paid: false, - enable_comment_notifications: true + enable_comment_notifications: true, + email_suppression: { + suppressed: false, + info: null + } }); });