diff --git a/apps/portal/README.md b/apps/portal/README.md
index fa568b4099..6f4591d74b 100644
--- a/apps/portal/README.md
+++ b/apps/portal/README.md
@@ -48,6 +48,8 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.
You will also see any lint errors in the console.
+Start the portal server when developing Ghost by running Ghost (in root folder) via `yarn dev --all` or `yarn dev --portal`. This will host the portal JavaScript files, and makes sure that Ghost uses these locally hosted assets instead of the ones from the CDN.
+
### `yarn build`
Creates the production single minified bundle for external use in `umd/portal.min.js`.
diff --git a/ghost/members-api/lib/repositories/MemberRepository.js b/ghost/members-api/lib/repositories/MemberRepository.js
index bbcd2d315f..3019f331e5 100644
--- a/ghost/members-api/lib/repositories/MemberRepository.js
+++ b/ghost/members-api/lib/repositories/MemberRepository.js
@@ -18,7 +18,8 @@ const messages = {
productNotFound: 'Could not find Product {id}',
bulkActionRequiresFilter: 'Cannot perform {action} without a filter or all=true',
tierArchived: 'Cannot use archived Tiers',
- invalidEmail: 'Invalid Email'
+ invalidEmail: 'Invalid Email',
+ invalidNewsletterId: 'Cannot subscribe to invalid newsletter {id}'
};
/**
@@ -281,7 +282,18 @@ module.exports = class MemberRepository {
memberStatusData.status = 'comped';
}
- // Subscribe member to default newsletters
+ //checks for custom signUp forms
+ if (memberData.newsletters && memberData.newsletters.length > 0) {
+ const savedNewsletter = await this._newslettersService.browse({filter: `id:'${memberData.newsletters[0].id}'`});
+ if (savedNewsletter.length === 0) {
+ throw new errors.BadRequestError({message: tpl(messages.invalidNewsletterId, {id: memberData.newsletters[0].id})});
+ }
+ if (savedNewsletter[0].status === 'archived') {
+ memberData.newsletters = [];
+ }
+ }
+
+ // Subscribe members to default newsletters
if (memberData.subscribed !== false && !memberData.newsletters) {
const browseOptions = _.pick(options, 'transacting');
memberData.newsletters = await this.getSubscribeOnSignupNewsletters(browseOptions);
diff --git a/ghost/members-api/test/unit/lib/repositories/member.test.js b/ghost/members-api/test/unit/lib/repositories/member.test.js
index 0baf70977d..35b639013b 100644
--- a/ghost/members-api/test/unit/lib/repositories/member.test.js
+++ b/ghost/members-api/test/unit/lib/repositories/member.test.js
@@ -310,4 +310,134 @@ describe('MemberRepository', function () {
})).should.be.true();
});
});
+
+ describe('create', function () {
+ let memberStub, memberModelStub, newslettersServiceStub;
+
+ beforeEach(function () {
+ memberStub = {
+ get: sinon.stub(),
+ related: sinon.stub()
+ };
+
+ memberStub.related
+ .withArgs('products').returns({
+ models: []
+ })
+ .withArgs('newsletters').returns({
+ models: []
+ });
+
+ memberModelStub = {
+ add: sinon.stub().resolves(memberStub)
+ };
+
+ newslettersServiceStub = {
+ browse: sinon.stub()
+ };
+ });
+
+ it('subscribes a member to a specified newsletter', async function () {
+ const newsletter = {
+ id: 'abc123',
+ status: 'active'
+ };
+
+ newslettersServiceStub.browse
+ .withArgs({
+ filter: `id:'${newsletter.id}'`
+ })
+ .resolves([newsletter]);
+
+ const repo = new MemberRepository({
+ Member: memberModelStub,
+ MemberStatusEvent: {
+ add: sinon.stub().resolves()
+ },
+ MemberSubscribeEvent: {
+ add: sinon.stub().resolves()
+ },
+ newslettersService: newslettersServiceStub
+ });
+
+ await repo.create({
+ email: 'jamie@example.com',
+ email_disabled: false,
+ newsletters: [
+ {id: newsletter.id}
+ ]
+ });
+
+ newslettersServiceStub.browse.calledOnce.should.be.true();
+ memberModelStub.add.calledOnce.should.be.true();
+ memberModelStub.add.args[0][0].newsletters.should.eql([
+ {id: newsletter.id}
+ ]);
+ });
+
+ it('does not allow a member to be subscribed to an invalid newsletter', async function () {
+ const INVALID_NEWSLETTER_ID = 'abc123';
+
+ newslettersServiceStub.browse
+ .withArgs({
+ filter: `id:'${INVALID_NEWSLETTER_ID}'`
+ })
+ .resolves([]);
+
+ const repo = new MemberRepository({
+ Member: memberModelStub,
+ MemberStatusEvent: {
+ add: sinon.stub().resolves()
+ },
+ MemberSubscribeEvent: {
+ add: sinon.stub().resolves()
+ },
+ newslettersService: newslettersServiceStub
+ });
+
+ await repo.create({
+ email: 'jamie@example.com',
+ email_disabled: false,
+ newsletters: [
+ {id: INVALID_NEWSLETTER_ID}
+ ]
+ }).should.be.rejectedWith(`Cannot subscribe to invalid newsletter ${INVALID_NEWSLETTER_ID}`);
+ });
+
+ it('does not subscribe a member to an archived newsletter', async function () {
+ const newsletter = {
+ id: 'abc123',
+ status: 'archived'
+ };
+
+ newslettersServiceStub.browse
+ .withArgs({
+ filter: `id:'${newsletter.id}'`
+ })
+ .resolves([newsletter]);
+
+ const repo = new MemberRepository({
+ Member: memberModelStub,
+ MemberStatusEvent: {
+ add: sinon.stub().resolves()
+ },
+ MemberSubscribeEvent: {
+ add: sinon.stub().resolves()
+ },
+ newslettersService: newslettersServiceStub
+ });
+
+ await repo.create({
+ email: 'jamie@example.com',
+ email_disabled: false,
+ newsletters: [
+ {id: newsletter.id}
+ ]
+ });
+
+ newslettersServiceStub.browse.calledOnce.should.be.true();
+ memberModelStub.add.calledOnce.should.be.true();
+ memberModelStub.add.args[0][0].newsletters.should.eql([]);
+ });
+ });
});