mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-15 03:01:37 -05:00
✨ Added support for gating content by member labels and products (#12946)
refs https://github.com/TryGhost/Team/issues/581 closes https://github.com/TryGhost/Team/issues/582 Emails can now be sent to members with specific associated labels or products by specifying an NQL string. We want to bring the same members segment feature to content by allowing `visibility` to be an NQL filter string on top of the `public/members/paid` special-case strings. As an example it's possible to set `posts.visibility` to `label:vip` to make a post available only to those members with the `vip` label. - removed enum validations for `visibility` so it now accepts any string or `null` - bumped `@tryghost/admin-api-schema` for API-level validation changes - added nql validation to API input validators by running the visibility query against the members model - added transform of NQL to special-case visibility values when saving post model - ensures there's a single way of representing "members" and "paid" where NQL gives multiple ways of representing the same segment - useful for keeping theme-level checks such as `{{#has visibility="paid"}}` working as expected - updated content-gating to parse nql from post's visibility and use it to query the currently logged in member to see if there's a match - bumped @tryghost/members-api to include label and product data when loading member
This commit is contained in:
parent
cfaddf82e8
commit
c36e749820
25 changed files with 728 additions and 137 deletions
|
@ -1,6 +1,42 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const models = require('../../../../../models');
|
||||
const {ValidationError} = require('@tryghost/errors');
|
||||
const i18n = require('../../../../../../shared/i18n');
|
||||
|
||||
const validateVisibility = async function (frame) {
|
||||
if (!frame.data.pages || !frame.data.pages[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
||||
const visibility = frame.data.pages[0].visibility;
|
||||
if (visibility) {
|
||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
||||
// check filter is valid
|
||||
try {
|
||||
await models.Member.findPage({filter: visibility, limit: 1});
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(new ValidationError({
|
||||
message: i18n.t('errors.api.pages.invalidVisibilityFilter'),
|
||||
property: 'visibility'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
add: jsonSchema.validate,
|
||||
edit: jsonSchema.validate
|
||||
add(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
},
|
||||
edit(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,42 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const models = require('../../../../../models');
|
||||
const {ValidationError} = require('@tryghost/errors');
|
||||
const i18n = require('../../../../../../shared/i18n');
|
||||
|
||||
const validateVisibility = async function (frame) {
|
||||
if (!frame.data.posts || !frame.data.posts[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
||||
const visibility = frame.data.posts[0].visibility;
|
||||
if (visibility) {
|
||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
||||
// check filter is valid
|
||||
try {
|
||||
await models.Member.findPage({filter: visibility, limit: 1});
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(new ValidationError({
|
||||
message: i18n.t('errors.api.posts.invalidVisibilityFilter'),
|
||||
property: 'visibility'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
add: jsonSchema.validate,
|
||||
edit: jsonSchema.validate
|
||||
add(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
},
|
||||
edit(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,42 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const models = require('../../../../../models');
|
||||
const {ValidationError} = require('@tryghost/errors');
|
||||
const i18n = require('../../../../../../shared/i18n');
|
||||
|
||||
const validateVisibility = async function (frame) {
|
||||
if (!frame.data.pages || !frame.data.pages[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
||||
const visibility = frame.data.pages[0].visibility;
|
||||
if (visibility) {
|
||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
||||
// check filter is valid
|
||||
try {
|
||||
await models.Member.findPage({filter: visibility, limit: 1});
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(new ValidationError({
|
||||
message: i18n.t('errors.api.pages.invalidVisibilityFilter'),
|
||||
property: 'visibility'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
add: jsonSchema.validate,
|
||||
edit: jsonSchema.validate
|
||||
add(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
},
|
||||
edit(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,42 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const models = require('../../../../../models');
|
||||
const {ValidationError} = require('@tryghost/errors');
|
||||
const i18n = require('../../../../../../shared/i18n');
|
||||
|
||||
const validateVisibility = async function (frame) {
|
||||
if (!frame.data.posts || !frame.data.posts[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
||||
const visibility = frame.data.posts[0].visibility;
|
||||
if (visibility) {
|
||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
||||
// check filter is valid
|
||||
try {
|
||||
await models.Member.findPage({filter: visibility, limit: 1});
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(new ValidationError({
|
||||
message: i18n.t('errors.api.posts.invalidVisibilityFilter'),
|
||||
property: 'visibility'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
add: jsonSchema.validate,
|
||||
edit: jsonSchema.validate
|
||||
add(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
},
|
||||
edit(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,42 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const models = require('../../../../../models');
|
||||
const {ValidationError} = require('@tryghost/errors');
|
||||
const i18n = require('../../../../../../shared/i18n');
|
||||
|
||||
const validateVisibility = async function (frame) {
|
||||
if (!frame.data.pages || !frame.data.pages[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
||||
const visibility = frame.data.pages[0].visibility;
|
||||
if (visibility) {
|
||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
||||
// check filter is valid
|
||||
try {
|
||||
await models.Member.findPage({filter: visibility, limit: 1});
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(new ValidationError({
|
||||
message: i18n.t('errors.api.pages.invalidVisibilityFilter'),
|
||||
property: 'visibility'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
add: jsonSchema.validate,
|
||||
edit: jsonSchema.validate
|
||||
add(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
},
|
||||
edit(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,42 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const models = require('../../../../../models');
|
||||
const {ValidationError} = require('@tryghost/errors');
|
||||
const i18n = require('../../../../../../shared/i18n');
|
||||
|
||||
const validateVisibility = async function (frame) {
|
||||
if (!frame.data.posts || !frame.data.posts[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
||||
const visibility = frame.data.posts[0].visibility;
|
||||
if (visibility) {
|
||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
||||
// check filter is valid
|
||||
try {
|
||||
await models.Member.findPage({filter: visibility, limit: 1});
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(new ValidationError({
|
||||
message: i18n.t('errors.api.posts.invalidVisibilityFilter'),
|
||||
property: 'visibility'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
add: jsonSchema.validate,
|
||||
edit: jsonSchema.validate
|
||||
add(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
},
|
||||
edit(apiConfig, frame) {
|
||||
return jsonSchema.validate(...arguments).then(() => {
|
||||
return validateVisibility(frame);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -27,8 +27,7 @@ module.exports = {
|
|||
type: 'string',
|
||||
maxlength: 50,
|
||||
nullable: false,
|
||||
defaultTo: 'public',
|
||||
validations: {isIn: [['public', 'members', 'paid']]}
|
||||
defaultTo: 'public'
|
||||
},
|
||||
email_recipient_filter: {
|
||||
type: 'string',
|
||||
|
|
|
@ -6,6 +6,7 @@ const Promise = require('bluebird');
|
|||
const {sequence} = require('@tryghost/promise');
|
||||
const i18n = require('../../shared/i18n');
|
||||
const errors = require('@tryghost/errors');
|
||||
const nql = require('@nexes/nql');
|
||||
const htmlToPlaintext = require('../../shared/html-to-plaintext');
|
||||
const ghostBookshelf = require('./base');
|
||||
const config = require('../../shared/config');
|
||||
|
@ -155,6 +156,20 @@ Post = ghostBookshelf.Model.extend({
|
|||
attrs.email_recipient_filter = 'status:-free';
|
||||
}
|
||||
|
||||
// transform visibility NQL queries to special-case values where necessary
|
||||
// ensures checks against special-case values such as `{{#has visibility="paid"}}` continue working
|
||||
if (attrs.visibility && !['public', 'members', 'paid'].includes(attrs.visibility)) {
|
||||
if (attrs.visibility === 'status:-free') {
|
||||
attrs.visibility = 'paid';
|
||||
} else {
|
||||
const visibilityNql = nql(attrs.visibility);
|
||||
|
||||
if (visibilityNql.queryJSON({status: 'free'}) && visibilityNql.queryJSON({status: '-free'})) {
|
||||
attrs.visibility = 'members';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
const nql = require('@nexes/nql');
|
||||
|
||||
// @ts-check
|
||||
/** @typedef { boolean } AccessFlag */
|
||||
|
||||
const PERMIT_ACCESS = true;
|
||||
const BLOCK_ACCESS = false;
|
||||
|
||||
// TODO: better place to store this?
|
||||
const MEMBER_NQL_EXPANSIONS = [{
|
||||
key: 'labels',
|
||||
replacement: 'labels.slug'
|
||||
}, {
|
||||
key: 'label',
|
||||
replacement: 'labels.slug'
|
||||
}, {
|
||||
key: 'products',
|
||||
replacement: 'products.slug'
|
||||
}, {
|
||||
key: 'product',
|
||||
replacement: 'products.slug'
|
||||
}];
|
||||
|
||||
/**
|
||||
* @param {object} post - A post object to check access to
|
||||
* @param {object} member - The member whos access should be checked
|
||||
|
@ -23,7 +40,9 @@ function checkPostAccess(post, member) {
|
|||
return PERMIT_ACCESS;
|
||||
}
|
||||
|
||||
if (post.visibility === 'paid' && (member.status === 'paid' || member.status === 'comped' || member.comped)) {
|
||||
const visibility = post.visibility === 'paid' ? 'status:-free' : post.visibility;
|
||||
|
||||
if (visibility && member.status && nql(visibility, {expansions: MEMBER_NQL_EXPANSIONS}).queryJSON(member)) {
|
||||
return PERMIT_ACCESS;
|
||||
}
|
||||
|
||||
|
|
|
@ -356,13 +356,15 @@
|
|||
},
|
||||
"posts": {
|
||||
"postNotFound": "Post not found.",
|
||||
"invalidEmailRecipientFilter": "Invalid filter in email_recipient_filter param."
|
||||
"invalidEmailRecipientFilter": "Invalid filter in email_recipient_filter param.",
|
||||
"invalidVisibilityFilter": "Invalid filter in visibility property"
|
||||
},
|
||||
"authors": {
|
||||
"notFound": "Author not found."
|
||||
},
|
||||
"pages": {
|
||||
"pageNotFound": "Page not found."
|
||||
"pageNotFound": "Page not found.",
|
||||
"invalidVisibilityFilter": "Invalid filter in visibility property"
|
||||
},
|
||||
"job": {
|
||||
"notFound": "Job not found.",
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"@nexes/nql": "0.5.2",
|
||||
"@sentry/node": "6.3.6",
|
||||
"@tryghost/adapter-manager": "0.2.12",
|
||||
"@tryghost/admin-api-schema": "2.1.1",
|
||||
"@tryghost/admin-api-schema": "2.2.1",
|
||||
"@tryghost/bootstrap-socket": "0.2.8",
|
||||
"@tryghost/constants": "0.1.7",
|
||||
"@tryghost/email-analytics-provider-mailgun": "1.0.0",
|
||||
|
@ -59,7 +59,7 @@
|
|||
"@tryghost/kg-mobiledoc-html-renderer": "4.0.0",
|
||||
"@tryghost/limit-service": "0.5.0",
|
||||
"@tryghost/magic-link": "1.0.2",
|
||||
"@tryghost/members-api": "1.4.0",
|
||||
"@tryghost/members-api": "1.5.0",
|
||||
"@tryghost/members-csv": "1.0.0",
|
||||
"@tryghost/members-ssr": "1.0.2",
|
||||
"@tryghost/mw-session-from-token": "0.1.20",
|
||||
|
|
|
@ -35,7 +35,7 @@ describe('Members API', function () {
|
|||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(5);
|
||||
jsonResponse.members.should.have.length(8);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'subscriptions');
|
||||
|
||||
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
|
||||
|
@ -44,7 +44,7 @@ describe('Members API', function () {
|
|||
jsonResponse.meta.pagination.should.have.property('page', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('limit', 15);
|
||||
jsonResponse.meta.pagination.should.have.property('pages', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 5);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 8);
|
||||
jsonResponse.meta.pagination.should.have.property('next', null);
|
||||
jsonResponse.meta.pagination.should.have.property('prev', null);
|
||||
});
|
||||
|
@ -98,7 +98,7 @@ describe('Members API', function () {
|
|||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(2);
|
||||
jsonResponse.members.should.have.length(4);
|
||||
jsonResponse.members[0].email.should.equal('paid@test.com');
|
||||
jsonResponse.members[1].email.should.equal('trialing@test.com');
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
|
|
|
@ -12,6 +12,26 @@ const settingsCache = require('../../core/server/services/settings/cache');
|
|||
describe('Front-end members behaviour', function () {
|
||||
let request;
|
||||
|
||||
async function loginAsMember(email) {
|
||||
// membersService needs to be required after Ghost start so that settings
|
||||
// are pre-populated with defaults
|
||||
const membersService = require('../../core/server/services/members');
|
||||
|
||||
const signinLink = await membersService.api.getMagicLink(email);
|
||||
const signinURL = new URL(signinLink);
|
||||
// request needs a relative path rather than full url with host
|
||||
const signinPath = `${signinURL.pathname}${signinURL.search}`;
|
||||
|
||||
// perform a sign-in request to set members cookies on superagent
|
||||
await request.get(signinPath)
|
||||
.expect(302)
|
||||
.then((res) => {
|
||||
const redirectUrl = new URL(res.headers.location, testUtils.API.getURL());
|
||||
should.exist(redirectUrl.searchParams.get('success'));
|
||||
redirectUrl.searchParams.get('success').should.eql('true');
|
||||
});
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
const originalSettingsCacheGetFn = settingsCache.get;
|
||||
|
||||
|
@ -114,6 +134,8 @@ describe('Front-end members behaviour', function () {
|
|||
let membersPost;
|
||||
let paidPost;
|
||||
let membersPostWithPaywallCard;
|
||||
let labelPost;
|
||||
let productPost;
|
||||
|
||||
before(function () {
|
||||
publicPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
|
@ -142,11 +164,25 @@ describe('Front-end members behaviour', function () {
|
|||
published_at: moment().add(5, 'seconds').toDate()
|
||||
});
|
||||
|
||||
labelPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-must-be-labelled-vip',
|
||||
visibility: 'label:vip',
|
||||
published_at: moment().toDate()
|
||||
});
|
||||
|
||||
productPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-must-have-default-product',
|
||||
visibility: 'product:default-product',
|
||||
published_at: moment().toDate()
|
||||
});
|
||||
|
||||
return testUtils.fixtures.insertPosts([
|
||||
publicPost,
|
||||
membersPost,
|
||||
paidPost,
|
||||
membersPostWithPaywallCard
|
||||
membersPostWithPaywallCard,
|
||||
labelPost,
|
||||
productPost
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -168,6 +204,7 @@ describe('Front-end members behaviour', function () {
|
|||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read paid post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-be-paid-for/')
|
||||
|
@ -176,27 +213,29 @@ describe('Front-end members behaviour', function () {
|
|||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read label-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-be-labelled-vip/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read product-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-have-default-product/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as free member', function () {
|
||||
before(async function () {
|
||||
// membersService needs to be required after Ghost start so that settings
|
||||
// are pre-populated with defaults
|
||||
const membersService = require('../../core/server/services/members');
|
||||
|
||||
const signinLink = await membersService.api.getMagicLink('member1@test.com');
|
||||
const signinURL = new URL(signinLink);
|
||||
// request needs a relative path rather than full url with host
|
||||
const signinPath = `${signinURL.pathname}${signinURL.search}`;
|
||||
|
||||
// perform a sign-in request to set members cookies on superagent
|
||||
await request.get(signinPath)
|
||||
.expect(302)
|
||||
.then((res) => {
|
||||
const redirectUrl = new URL(res.headers.location, testUtils.API.getURL());
|
||||
should.exist(redirectUrl.searchParams.get('success'));
|
||||
redirectUrl.searchParams.get('success').should.eql('true');
|
||||
});
|
||||
await loginAsMember('member1@test.com');
|
||||
});
|
||||
|
||||
it('can read public post content', function () {
|
||||
|
@ -225,6 +264,39 @@ describe('Front-end members behaviour', function () {
|
|||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read label-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-be-labelled-vip/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read product-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-have-default-product/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as free member with vip label', function () {
|
||||
before(async function () {
|
||||
await loginAsMember('vip@test.com');
|
||||
});
|
||||
|
||||
it('can read label-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-be-labelled-vip/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as paid member', function () {
|
||||
|
@ -274,27 +346,44 @@ describe('Front-end members behaviour', function () {
|
|||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read label-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-be-labelled-vip/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read product-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-have-default-product/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as paid member with vip label', function () {
|
||||
before(async function () {
|
||||
await loginAsMember('vip-paid@test.com');
|
||||
});
|
||||
|
||||
it('can read label-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-be-labelled-vip/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as comped member', function () {
|
||||
before(async function () {
|
||||
// membersService needs to be required after Ghost start so that settings
|
||||
// are pre-populated with defaults
|
||||
const membersService = require('../../core/server/services/members');
|
||||
|
||||
const signinLink = await membersService.api.getMagicLink('comped@test.com');
|
||||
const signinURL = new URL(signinLink);
|
||||
// request needs a relative path rather than full url with host
|
||||
const signinPath = `${signinURL.pathname}${signinURL.search}`;
|
||||
|
||||
// perform a sign-in request to set members cookies on superagent
|
||||
await request.get(signinPath)
|
||||
.expect(302)
|
||||
.then((res) => {
|
||||
const redirectUrl = new URL(res.headers.location, testUtils.API.getURL());
|
||||
should.exist(redirectUrl.searchParams.get('success'));
|
||||
redirectUrl.searchParams.get('success').should.eql('true');
|
||||
});
|
||||
await loginAsMember('comped@test.com');
|
||||
});
|
||||
|
||||
it('can read public post content', function () {
|
||||
|
@ -323,6 +412,39 @@ describe('Front-end members behaviour', function () {
|
|||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read label-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-be-labelled-vip/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read product-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-have-default-product/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as member with product', function () {
|
||||
before(async function () {
|
||||
await loginAsMember('with-product@test.com');
|
||||
});
|
||||
|
||||
it('can read product-only post content', function () {
|
||||
return request
|
||||
.get('/thou-must-have-default-product/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ const ghost = testUtils.startGhost;
|
|||
|
||||
let request;
|
||||
|
||||
describe('Members API', function () {
|
||||
describe('Members API (canary)', function () {
|
||||
before(function () {
|
||||
sinon.stub(labs, 'isSet').withArgs('members').returns(true);
|
||||
});
|
||||
|
@ -96,7 +96,7 @@ describe('Members API', function () {
|
|||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.members);
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(5);
|
||||
jsonResponse.members.should.have.length(8);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('paid@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(80);
|
||||
|
@ -117,7 +117,7 @@ describe('Members API', function () {
|
|||
.then((res) => {
|
||||
const jsonResponse = res.body;
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(5);
|
||||
jsonResponse.members.should.have.length(8);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('member2@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(50);
|
||||
|
|
|
@ -96,7 +96,7 @@ describe('Members API', function () {
|
|||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.members);
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(5);
|
||||
jsonResponse.members.should.have.length(8);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('paid@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(80);
|
||||
|
@ -117,7 +117,7 @@ describe('Members API', function () {
|
|||
.then((res) => {
|
||||
const jsonResponse = res.body;
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(5);
|
||||
jsonResponse.members.should.have.length(8);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('member2@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(50);
|
||||
|
@ -635,8 +635,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
// 8 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(14);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -659,8 +659,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
// 8 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(14);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -683,8 +683,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
// 8 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(14);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -733,6 +733,44 @@ describe('Post Model', function () {
|
|||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('transforms special-case visibility values on save', function (done) {
|
||||
// status:-free === paid
|
||||
// status:-free,status:free (+variations) === members
|
||||
|
||||
const postId = testUtils.DataGenerator.Content.posts[3].id;
|
||||
|
||||
models.Post.findOne({id: postId}).then(() => {
|
||||
return models.Post.edit({
|
||||
visibility: 'status:-free'
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then((edited) => {
|
||||
edited.attributes.visibility.should.equal('paid');
|
||||
return db.knex('posts').where({id: edited.id});
|
||||
}).then((knexResult) => {
|
||||
const [knexPost] = knexResult;
|
||||
knexPost.visibility.should.equal('paid');
|
||||
}).then(() => {
|
||||
return models.Post.edit({
|
||||
visibility: 'status:-free,status:free'
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then((edited) => {
|
||||
edited.attributes.visibility.should.equal('members');
|
||||
|
||||
return models.Post.edit({
|
||||
visibility: 'status:free,status:-free'
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then((edited) => {
|
||||
edited.attributes.visibility.should.equal('members');
|
||||
|
||||
return models.Post.edit({
|
||||
visibility: 'status:free,status:-free,label:vip'
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then((edited) => {
|
||||
edited.attributes.visibility.should.equal('members');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add', function () {
|
||||
|
|
|
@ -4,8 +4,18 @@ const should = require('should');
|
|||
const sinon = require('sinon');
|
||||
const Promise = require('bluebird');
|
||||
const validators = require('../../../../../../../core/server/api/canary/utils/validators');
|
||||
const models = require('../../../../../../../core/server/models');
|
||||
|
||||
describe('Unit: canary/utils/validators/input/pages', function () {
|
||||
before(function () {
|
||||
return models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
const memberFindPageStub = sinon.stub(models.Member, 'findPage').returns(Promise.reject());
|
||||
memberFindPageStub.withArgs({filter: 'label:vip', limit: 1}).returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
@ -180,6 +190,21 @@ describe('Unit: canary/utils/validators/input/pages', function () {
|
|||
return Promise.all(checks);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass for valid NQL visibility', function () {
|
||||
const frame = {
|
||||
options: {},
|
||||
data: {
|
||||
pages: [{
|
||||
title: 'pass',
|
||||
authors: [{id: 'correct'}],
|
||||
visibility: 'label:vip'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
return validators.input.pages.add(apiConfig, frame);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authors structure', function () {
|
||||
|
|
|
@ -4,8 +4,18 @@ const should = require('should');
|
|||
const sinon = require('sinon');
|
||||
const Promise = require('bluebird');
|
||||
const validators = require('../../../../../../../core/server/api/canary/utils/validators');
|
||||
const models = require('../../../../../../../core/server/models');
|
||||
|
||||
describe('Unit: canary/utils/validators/input/posts', function () {
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
const memberFindPageStub = sinon.stub(models.Member, 'findPage').returns(Promise.reject());
|
||||
memberFindPageStub.withArgs({filter: 'label:vip', limit: 1}).returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
@ -180,6 +190,21 @@ describe('Unit: canary/utils/validators/input/posts', function () {
|
|||
return Promise.all(checks);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass for valid NQL visibility', function () {
|
||||
const frame = {
|
||||
options: {},
|
||||
data: {
|
||||
posts: [{
|
||||
title: 'pass',
|
||||
authors: [{id: 'correct'}],
|
||||
visibility: 'label:vip'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
return validators.input.posts.add(apiConfig, frame);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authors structure', function () {
|
||||
|
|
|
@ -4,8 +4,18 @@ const should = require('should');
|
|||
const sinon = require('sinon');
|
||||
const Promise = require('bluebird');
|
||||
const validators = require('../../../../../../../core/server/api/v2/utils/validators');
|
||||
const models = require('../../../../../../../core/server/models');
|
||||
|
||||
describe('Unit: v2/utils/validators/input/pages', function () {
|
||||
before(function () {
|
||||
return models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
const memberFindPageStub = sinon.stub(models.Member, 'findPage').returns(Promise.reject());
|
||||
memberFindPageStub.withArgs({filter: 'label:vip', limit: 1}).returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
@ -180,6 +190,21 @@ describe('Unit: v2/utils/validators/input/pages', function () {
|
|||
return Promise.all(checks);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass for valid NQL visibility', function () {
|
||||
const frame = {
|
||||
options: {},
|
||||
data: {
|
||||
pages: [{
|
||||
title: 'pass',
|
||||
authors: [{id: 'correct'}],
|
||||
visibility: 'label:vip'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
return validators.input.pages.add(apiConfig, frame);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authors structure', function () {
|
||||
|
|
|
@ -4,8 +4,18 @@ const should = require('should');
|
|||
const sinon = require('sinon');
|
||||
const Promise = require('bluebird');
|
||||
const validators = require('../../../../../../../core/server/api/v2/utils/validators');
|
||||
const models = require('../../../../../../../core/server/models');
|
||||
|
||||
describe('Unit: v2/utils/validators/input/posts', function () {
|
||||
before(function () {
|
||||
return models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
const memberFindPageStub = sinon.stub(models.Member, 'findPage').returns(Promise.reject());
|
||||
memberFindPageStub.withArgs({filter: 'label:vip', limit: 1}).returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
@ -180,6 +190,21 @@ describe('Unit: v2/utils/validators/input/posts', function () {
|
|||
return Promise.all(checks);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass for valid NQL visibility', function () {
|
||||
const frame = {
|
||||
options: {},
|
||||
data: {
|
||||
posts: [{
|
||||
title: 'pass',
|
||||
authors: [{id: 'correct'}],
|
||||
visibility: 'label:vip'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
return validators.input.posts.add(apiConfig, frame);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authors structure', function () {
|
||||
|
|
|
@ -4,8 +4,18 @@ const should = require('should');
|
|||
const sinon = require('sinon');
|
||||
const Promise = require('bluebird');
|
||||
const validators = require('../../../../../../../core/server/api/v3/utils/validators');
|
||||
const models = require('../../../../../../../core/server/models');
|
||||
|
||||
describe('Unit: v3/utils/validators/input/pages', function () {
|
||||
before(function () {
|
||||
return models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
const memberFindPageStub = sinon.stub(models.Member, 'findPage').returns(Promise.reject());
|
||||
memberFindPageStub.withArgs({filter: 'label:vip', limit: 1}).returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
@ -180,6 +190,21 @@ describe('Unit: v3/utils/validators/input/pages', function () {
|
|||
return Promise.all(checks);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass for valid NQL visibility', function () {
|
||||
const frame = {
|
||||
options: {},
|
||||
data: {
|
||||
pages: [{
|
||||
title: 'pass',
|
||||
authors: [{id: 'correct'}],
|
||||
visibility: 'label:vip'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
return validators.input.pages.add(apiConfig, frame);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authors structure', function () {
|
||||
|
|
|
@ -4,8 +4,18 @@ const should = require('should');
|
|||
const sinon = require('sinon');
|
||||
const Promise = require('bluebird');
|
||||
const validators = require('../../../../../../../core/server/api/v3/utils/validators');
|
||||
const models = require('../../../../../../../core/server/models');
|
||||
|
||||
describe('Unit: v3/utils/validators/input/posts', function () {
|
||||
before(function () {
|
||||
return models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
const memberFindPageStub = sinon.stub(models.Member, 'findPage').returns(Promise.reject());
|
||||
memberFindPageStub.withArgs({filter: 'label:vip', limit: 1}).returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
@ -180,6 +190,21 @@ describe('Unit: v3/utils/validators/input/posts', function () {
|
|||
return Promise.all(checks);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass for valid NQL visibility', function () {
|
||||
const frame = {
|
||||
options: {},
|
||||
data: {
|
||||
posts: [{
|
||||
title: 'pass',
|
||||
authors: [{id: 'correct'}],
|
||||
visibility: 'label:vip'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
return validators.input.posts.add(apiConfig, frame);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authors structure', function () {
|
||||
|
|
|
@ -457,34 +457,40 @@ const fixtures = {
|
|||
});
|
||||
},
|
||||
|
||||
insertMembersAndLabels: function insertMembersAndLabels() {
|
||||
insertMembersAndLabelsAndProducts: function insertMembersAndLabelsAndProducts() {
|
||||
return Promise.map(DataGenerator.forKnex.labels, function (label) {
|
||||
return models.Label.add(label, context.internal);
|
||||
}).then(function () {
|
||||
return Promise.each(_.cloneDeep(DataGenerator.forKnex.members), function (member) {
|
||||
let memberLabelRelations = _.filter(DataGenerator.forKnex.members_labels, {member_id: member.id});
|
||||
|
||||
memberLabelRelations = _.map(memberLabelRelations, function (memberLabelRelation) {
|
||||
return _.find(DataGenerator.forKnex.labels, {id: memberLabelRelation.label_id});
|
||||
});
|
||||
|
||||
member.labels = memberLabelRelations;
|
||||
|
||||
return models.Member.add(member, context.internal);
|
||||
});
|
||||
}).then(function () {
|
||||
return Promise.each(_.cloneDeep(DataGenerator.forKnex.members_stripe_customers), function (customer) {
|
||||
return models.MemberStripeCustomer.add(customer, context.internal);
|
||||
});
|
||||
}).then(function () {
|
||||
let productsToInsert = fixtureUtils.findModelFixtures('Product').entries;
|
||||
return Promise.map(productsToInsert, product => models.Product.add(product, context.internal));
|
||||
}).then(function () {
|
||||
return models.Product.findOne({}, context.internal);
|
||||
}).then(function (product) {
|
||||
return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_products), function (stripeProduct) {
|
||||
stripeProduct.product_id = product.id;
|
||||
return models.StripeProduct.add(stripeProduct, context.internal);
|
||||
return Promise.props({
|
||||
stripeProducts: Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_products), function (stripeProduct) {
|
||||
stripeProduct.product_id = product.id;
|
||||
return models.StripeProduct.add(stripeProduct, context.internal);
|
||||
}),
|
||||
members: Promise.each(_.cloneDeep(DataGenerator.forKnex.members), function (member) {
|
||||
let memberLabelRelations = _.filter(DataGenerator.forKnex.members_labels, {member_id: member.id});
|
||||
|
||||
memberLabelRelations = _.map(memberLabelRelations, function (memberLabelRelation) {
|
||||
return _.find(DataGenerator.forKnex.labels, {id: memberLabelRelation.label_id});
|
||||
});
|
||||
|
||||
member.labels = memberLabelRelations;
|
||||
|
||||
// TODO: replace with full member/product associations
|
||||
if (member.email === 'with-product@test.com') {
|
||||
member.products = [{slug: product.get('slug')}];
|
||||
}
|
||||
|
||||
return models.Member.add(member, context.internal);
|
||||
})
|
||||
});
|
||||
}).then(function () {
|
||||
return Promise.each(_.cloneDeep(DataGenerator.forKnex.members_stripe_customers), function (customer) {
|
||||
return models.MemberStripeCustomer.add(customer, context.internal);
|
||||
});
|
||||
}).then(function () {
|
||||
return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_prices), function (stripePrice) {
|
||||
|
@ -541,8 +547,8 @@ const toDoList = {
|
|||
member: function insertMember() {
|
||||
return fixtures.insertOne('Member', 'members', 'createMember');
|
||||
},
|
||||
members: function insertMembersAndLabels() {
|
||||
return fixtures.insertMembersAndLabels();
|
||||
members: function insertMembersAndLabelsAndProducts() {
|
||||
return fixtures.insertMembersAndLabelsAndProducts();
|
||||
},
|
||||
'members:emails': function insertEmailsAndRecipients() {
|
||||
return fixtures.insertEmailsAndRecipients();
|
||||
|
|
|
@ -341,6 +341,27 @@ DataGenerator.Content = {
|
|||
name: 'Vinz Clortho',
|
||||
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b344',
|
||||
status: 'comped'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
email: 'vip@test.com',
|
||||
name: 'Winston Zeddemore',
|
||||
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b345',
|
||||
status: 'free'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
email: 'vip-paid@test.com',
|
||||
name: 'Peter Venkman',
|
||||
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b346',
|
||||
status: 'paid'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
email: 'with-product@test.com',
|
||||
name: 'Dana Barrett',
|
||||
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b347',
|
||||
status: 'paid'
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -362,6 +383,11 @@ DataGenerator.Content = {
|
|||
id: ObjectId().toHexString(),
|
||||
name: 'Label 2',
|
||||
slug: 'label-2'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
name: 'VIP',
|
||||
slug: 'vip'
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -381,10 +407,25 @@ DataGenerator.Content = {
|
|||
email: 'trialing@test.com'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
member_id: null, // relation added later
|
||||
customer_id: 'cus_HR3tBmNhx4QsZ0',
|
||||
name: 'Vinz Clortho',
|
||||
email: 'comped@test.com'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
member_id: null, // relation added later
|
||||
customer_id: 'cus_HR3tBmNhx4QsZ1',
|
||||
name: 'Peter Venkman',
|
||||
email: 'vip-paid@test.com'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
member_id: null, // relation added later
|
||||
customer_id: 'cus_HR3tBmNhx4QsZ2',
|
||||
name: 'Dana Barrett',
|
||||
email: 'with-product@test.com'
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -643,6 +684,8 @@ DataGenerator.Content.email_recipients[3].member_id = DataGenerator.Content.memb
|
|||
DataGenerator.Content.members_stripe_customers[0].member_id = DataGenerator.Content.members[2].id;
|
||||
DataGenerator.Content.members_stripe_customers[1].member_id = DataGenerator.Content.members[3].id;
|
||||
DataGenerator.Content.members_stripe_customers[2].member_id = DataGenerator.Content.members[4].id;
|
||||
DataGenerator.Content.members_stripe_customers[3].member_id = DataGenerator.Content.members[6].id;
|
||||
DataGenerator.Content.members_stripe_customers[4].member_id = DataGenerator.Content.members[7].id;
|
||||
|
||||
DataGenerator.forKnex = (function () {
|
||||
function createBasic(overrides) {
|
||||
|
@ -1166,17 +1209,29 @@ DataGenerator.forKnex = (function () {
|
|||
createMember(DataGenerator.Content.members[1]),
|
||||
createMember(DataGenerator.Content.members[2]),
|
||||
createMember(DataGenerator.Content.members[3]),
|
||||
createMember(DataGenerator.Content.members[4])
|
||||
createMember(DataGenerator.Content.members[4]),
|
||||
createMember(DataGenerator.Content.members[5]),
|
||||
createMember(DataGenerator.Content.members[6]),
|
||||
createMember(DataGenerator.Content.members[7])
|
||||
];
|
||||
|
||||
const labels = [
|
||||
createLabel(DataGenerator.Content.labels[0])
|
||||
createLabel(DataGenerator.Content.labels[0]),
|
||||
createLabel(DataGenerator.Content.labels[2])
|
||||
];
|
||||
|
||||
const members_labels = [
|
||||
createMembersLabels(
|
||||
DataGenerator.Content.members[0].id,
|
||||
DataGenerator.Content.labels[0].id
|
||||
),
|
||||
createMembersLabels(
|
||||
DataGenerator.Content.members[5].id,
|
||||
DataGenerator.Content.labels[2].id
|
||||
),
|
||||
createMembersLabels(
|
||||
DataGenerator.Content.members[6].id,
|
||||
DataGenerator.Content.labels[2].id
|
||||
)
|
||||
];
|
||||
|
||||
|
@ -1187,7 +1242,9 @@ DataGenerator.forKnex = (function () {
|
|||
const members_stripe_customers = [
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[0]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[1]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[2])
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[2]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[3]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[4])
|
||||
];
|
||||
|
||||
const stripe_products = [
|
||||
|
|
55
yarn.lock
55
yarn.lock
|
@ -564,10 +564,10 @@
|
|||
dependencies:
|
||||
"@tryghost/errors" "^0.2.11"
|
||||
|
||||
"@tryghost/admin-api-schema@2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/admin-api-schema/-/admin-api-schema-2.1.1.tgz#98670e9b1dfa028b14abf542a57b3d7caf26cab6"
|
||||
integrity sha512-UsYEggVR4X7oje+fMfizkSv+ThcAC/S9LtOcqbShhgI+xqNwQkueoQAQVpcHfA6y9kK/9wwGpJXaT5rM89WRjw==
|
||||
"@tryghost/admin-api-schema@2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/admin-api-schema/-/admin-api-schema-2.2.1.tgz#5d31abd194a5742d30b17ca230438a353b05b1aa"
|
||||
integrity sha512-FDNYefBGsCdJ0Y/Suil8snye+cchl5B/sU5gJ25rLBRrN2AD9zAJM0N27R1+6R93MUlwsggEKM7T/6GxNhMudQ==
|
||||
dependencies:
|
||||
"@tryghost/errors" "^0.2.10"
|
||||
bluebird "^3.5.3"
|
||||
|
@ -732,10 +732,10 @@
|
|||
jsonwebtoken "^8.5.1"
|
||||
lodash "^4.17.15"
|
||||
|
||||
"@tryghost/members-api@1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-1.4.0.tgz#4e801156e2bf1fa1f2917ab73ec6a14afefad183"
|
||||
integrity sha512-Wi0yKY1XHuYNR9CFsQD6Iro4gjHC++iZkQt5hYTCUrsTni7wK2B1dAtVcbq7/HdOoRJn2t4t7n1WmN3jsKbT9A==
|
||||
"@tryghost/members-api@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-1.5.0.tgz#f58fe7184283bda21f63f881f03a36cac8e1a7b9"
|
||||
integrity sha512-MLYRNE/BAG5pz7wAYY1PIzHTMG1pJ0SEzUhtI2ylAwq+QZWIvN/Hr62pMTXpxzCe1kCCI+Irw9lZlYnrbQDt8Q==
|
||||
dependencies:
|
||||
"@tryghost/errors" "^0.2.9"
|
||||
"@tryghost/magic-link" "^1.0.2"
|
||||
|
@ -748,7 +748,7 @@
|
|||
jsonwebtoken "^8.5.1"
|
||||
leaky-bucket "2.2.0"
|
||||
lodash "^4.17.11"
|
||||
node-jose "^1.1.3"
|
||||
node-jose "^2.0.0"
|
||||
stripe "^8.142.0"
|
||||
|
||||
"@tryghost/members-csv@1.0.0":
|
||||
|
@ -1640,13 +1640,6 @@ browser-stdout@1.3.1:
|
|||
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
|
||||
integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
|
||||
|
||||
browserify-zlib@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
|
||||
integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
|
||||
dependencies:
|
||||
pako "~1.0.5"
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.0:
|
||||
version "4.16.6"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
|
||||
|
@ -6877,11 +6870,6 @@ node-forge@^0.10.0:
|
|||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
|
||||
|
||||
node-forge@^0.8.5:
|
||||
version "0.8.5"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.5.tgz#57906f07614dc72762c84cef442f427c0e1b86ee"
|
||||
integrity sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==
|
||||
|
||||
node-gyp@3.x:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
|
||||
|
@ -6916,7 +6904,7 @@ node-gyp@^7.1.2:
|
|||
tar "^6.0.2"
|
||||
which "^2.0.2"
|
||||
|
||||
node-jose@2.0.0:
|
||||
node-jose@2.0.0, node-jose@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-jose/-/node-jose-2.0.0.tgz#541c6b52c387a3f18fc06cd502baad759af9c470"
|
||||
integrity sha512-j8zoFze1gijl8+DK/dSXXqX7+o2lMYv1XS+ptnXgGV/eloQaqq1YjNtieepbKs9jBS4WTnMOqyKSaQuunJzx0A==
|
||||
|
@ -6931,22 +6919,6 @@ node-jose@2.0.0:
|
|||
process "^0.11.10"
|
||||
uuid "^3.3.3"
|
||||
|
||||
node-jose@^1.1.3:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/node-jose/-/node-jose-1.1.4.tgz#af3f44a392e586d26b123b0e12dc09bef1e9863b"
|
||||
integrity sha512-L31IFwL3pWWcMHxxidCY51ezqrDXMkvlT/5pLTfNw5sXmmOLJuN6ug7txzF/iuZN55cRpyOmoJrotwBQIoo5Lw==
|
||||
dependencies:
|
||||
base64url "^3.0.1"
|
||||
browserify-zlib "^0.2.0"
|
||||
buffer "^5.5.0"
|
||||
es6-promise "^4.2.8"
|
||||
lodash "^4.17.15"
|
||||
long "^4.0.0"
|
||||
node-forge "^0.8.5"
|
||||
process "^0.11.10"
|
||||
react-zlib-js "^1.0.4"
|
||||
uuid "^3.3.3"
|
||||
|
||||
node-loggly-bulk@^2.2.4:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/node-loggly-bulk/-/node-loggly-bulk-2.2.5.tgz#6f41136f91b363d1b50612e8be0063859226967e"
|
||||
|
@ -7347,7 +7319,7 @@ pac-resolver@^3.0.0:
|
|||
netmask "^1.0.6"
|
||||
thunkify "^2.1.2"
|
||||
|
||||
pako@^1.0.11, pako@~1.0.5:
|
||||
pako@^1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||
|
@ -8145,11 +8117,6 @@ re2@^1.15.9:
|
|||
nan "^2.14.2"
|
||||
node-gyp "^7.1.2"
|
||||
|
||||
react-zlib-js@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.5.tgz#7bb433e1a4ae53a8e6f361b3d36166baf5bbc60f"
|
||||
integrity sha512-TLcPdmqhIl+ylwOwlfm1WUuI7NVvhAv3L74d1AabhjyaAbmLOROTA/Q4EQ/UMCFCOjIkVim9fT3UZOQSFk/mlA==
|
||||
|
||||
read-pkg-up@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
|
||||
|
|
Loading…
Add table
Reference in a new issue