ghost 4.32.2 → 4.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/boot.js +3 -0
- package/core/built/assets/{ghost-dark-67f6ba8347be37f997b3a7e430b29f72.css → ghost-dark-661a50922267648a0362c3d367a22013.css} +1 -1
- package/core/built/assets/{ghost.min-2d5f48403647d1e11805691cc7ad0835.css → ghost.min-1f0218f33e08f8d69b2159977d0c9318.css} +1 -1
- package/core/built/assets/{ghost.min-1f04d5d6f5f8f3739a86d5bc807cbe1c.js → ghost.min-501554f903f29164473a5dc620caaddb.js} +1285 -1229
- package/core/built/assets/img/apple-touch-icon-74680e326a7e87b159d366c7d4fb3d4b.png +0 -0
- package/core/built/assets/img/large-ac90af7c93a4b47e8d956fa9fef31d9d.png +0 -0
- package/core/built/assets/img/medium-fef07013cffd5c45a655a250912a0ad7.png +0 -0
- package/core/built/assets/img/small-b90396925485f17b2ca82c31be42de5f.png +0 -0
- package/core/built/assets/img/touch-icon-ipad-2e78629d62ad05746f980f14623dfadb.png +0 -0
- package/core/built/assets/img/touch-icon-iphone-93ed4382d391be9180093fd77ce8f410.png +0 -0
- package/core/built/assets/{vendor.min-df1ba725edbc456aa45af07d3443403e.js → vendor.min-d43620e98444a46441495445f4c155f8.js} +686 -731
- package/core/frontend/helpers/date.js +3 -4
- package/core/frontend/services/routing/config/canary.js +1 -1
- package/core/frontend/services/routing/config/v4.js +1 -1
- package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +3 -1
- package/core/frontend/web/site.js +5 -1
- package/core/server/api/canary/settings.js +2 -1
- package/core/server/api/canary/utils/serializers/output/products.js +4 -0
- package/core/server/data/db/info.js +4 -0
- package/core/server/data/migrations/versions/4.33/2022-01-14-11-50-add-type-column-to-products.js +12 -0
- package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +37 -0
- package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +46 -0
- package/core/server/data/migrations/versions/4.33/2022-01-19-10-43-add-active-column-to-products-table.js +7 -0
- package/core/server/data/schema/default-settings.json +1 -1
- package/core/server/data/schema/fixtures/fixtures.json +9 -1
- package/core/server/data/schema/schema.js +2 -0
- package/core/server/models/base/plugins/data-manipulation.js +3 -2
- package/core/server/models/product.js +4 -0
- package/core/server/models/tag.js +8 -0
- package/core/server/services/members/api.js +1 -15
- package/core/server/services/members/config.js +1 -9
- package/core/server/services/members/middleware.js +5 -3
- package/core/server/services/members/service.js +14 -28
- package/core/server/services/offers/service.js +1 -4
- package/core/server/services/public-config/config.js +3 -2
- package/core/server/services/stripe/config.js +24 -9
- package/core/server/services/stripe/index.js +1 -45
- package/core/server/services/stripe/service.js +58 -0
- package/core/server/update-check.js +2 -1
- package/core/server/web/admin/views/default-prod.html +9 -12
- package/core/server/web/admin/views/default.html +9 -12
- package/core/server/web/members/app.js +3 -2
- package/core/server/web/shared/middleware/cache-control.js +12 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +2 -1
- package/package.json +59 -57
- package/yarn.lock +652 -749
- package/core/built/assets/img/large-bf46e150380a4979a7389b45f5bb479d.png +0 -0
- package/core/built/assets/img/medium-7359075af28d69523987ff4c0e2067c5.png +0 -0
- package/core/built/assets/img/small-42ff134f320b8b5a6eca3781c4e4b2db.png +0 -0
- package/core/built/assets/img/touch-icon-ipad-3117c0fa950d0fc43c95becef61f4167.png +0 -0
- package/core/built/assets/img/touch-icon-iphone-d2790931c3477664981061ed9fa5242e.png +0 -0
|
@@ -25,12 +25,11 @@ module.exports = function (...attrs) {
|
|
|
25
25
|
// ensure that date is undefined, not null, as that can cause errors
|
|
26
26
|
date = date === null ? undefined : date;
|
|
27
27
|
|
|
28
|
-
const timezone = options.data.site.timezone;
|
|
29
|
-
const locale = options.data.site.locale;
|
|
30
|
-
|
|
31
28
|
const {
|
|
32
29
|
format = 'll',
|
|
33
|
-
timeago
|
|
30
|
+
timeago,
|
|
31
|
+
timezone = options.data.site.timezone,
|
|
32
|
+
locale = options.data.site.locale
|
|
34
33
|
} = options.hash;
|
|
35
34
|
|
|
36
35
|
const timeNow = moment().tz(timezone);
|
|
@@ -28,7 +28,9 @@ function calculateLegacyPriceData(products) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const defaultProduct = products
|
|
31
|
+
const defaultProduct = products.find((product) => {
|
|
32
|
+
return product.type === 'paid';
|
|
33
|
+
}) || {};
|
|
32
34
|
|
|
33
35
|
const monthlyPrice = makePriceObject(defaultProduct.monthly_price || defaultPrice);
|
|
34
36
|
|
|
@@ -125,7 +125,11 @@ module.exports = function setupSiteApp(options = {}) {
|
|
|
125
125
|
siteApp.use(membersService.middleware.loadMemberSession);
|
|
126
126
|
|
|
127
127
|
// /member/.well-known/* serves files (e.g. jwks.json) so it needs to be mounted before the prettyUrl mw to avoid trailing slashes
|
|
128
|
-
siteApp.use(
|
|
128
|
+
siteApp.use(
|
|
129
|
+
'/members/.well-known',
|
|
130
|
+
shared.middleware.cacheControl('public', {maxAge: 60 * 60 * 24}),
|
|
131
|
+
(req, res, next) => membersService.api.middleware.wellKnown(req, res, next)
|
|
132
|
+
);
|
|
129
133
|
|
|
130
134
|
// setup middleware for internal apps
|
|
131
135
|
// @TODO: refactor this to be a proper app middleware hook for internal apps
|
|
@@ -6,6 +6,7 @@ const tpl = require('@tryghost/tpl');
|
|
|
6
6
|
const {BadRequestError} = require('@tryghost/errors');
|
|
7
7
|
const settingsService = require('../../services/settings');
|
|
8
8
|
const membersService = require('../../services/members');
|
|
9
|
+
const stripeService = require('../../services/stripe');
|
|
9
10
|
|
|
10
11
|
const settingsBREADService = settingsService.getSettingsBREADServiceInstance();
|
|
11
12
|
|
|
@@ -132,7 +133,7 @@ module.exports = {
|
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
await
|
|
136
|
+
await stripeService.disconnect();
|
|
136
137
|
|
|
137
138
|
return models.Settings.edit([{
|
|
138
139
|
key: 'stripe_connect_publishable_key',
|
|
@@ -73,6 +73,8 @@ function serializeProduct(product, options, apiType) {
|
|
|
73
73
|
name: json.name,
|
|
74
74
|
description: json.description,
|
|
75
75
|
slug: json.slug,
|
|
76
|
+
active: json.active,
|
|
77
|
+
type: json.type,
|
|
76
78
|
created_at: json.created_at,
|
|
77
79
|
updated_at: json.updated_at,
|
|
78
80
|
stripe_prices: json.stripePrices ? json.stripePrices.map(price => serializeStripePrice(price, hideStripeData)) : null,
|
|
@@ -160,6 +162,8 @@ function createSerializer(debugString, serialize) {
|
|
|
160
162
|
* @prop {string} name
|
|
161
163
|
* @prop {string} slug
|
|
162
164
|
* @prop {string} description
|
|
165
|
+
* @prop {boolean} active
|
|
166
|
+
* @prop {string} type
|
|
163
167
|
* @prop {Date} created_at
|
|
164
168
|
* @prop {Date} updated_at
|
|
165
169
|
* @prop {StripePrice[]} [stripe_prices]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
2
|
+
const ObjectID = require('bson-objectid');
|
|
3
|
+
const {slugify} = require('@tryghost/string');
|
|
4
|
+
const logging = require('@tryghost/logging');
|
|
5
|
+
|
|
6
|
+
module.exports = createTransactionalMigration(
|
|
7
|
+
async function up(knex) {
|
|
8
|
+
const [result] = await knex
|
|
9
|
+
.count('id', {as: 'total'})
|
|
10
|
+
.from('products')
|
|
11
|
+
.where('type', 'free');
|
|
12
|
+
|
|
13
|
+
if (result.total !== 0) {
|
|
14
|
+
logging.warn(`Not adding default free tier, a free tier already exists`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const name = 'Free';
|
|
19
|
+
const id = ObjectID().toHexString();
|
|
20
|
+
|
|
21
|
+
logging.info(`Adding tier "${name}"`);
|
|
22
|
+
await knex('products')
|
|
23
|
+
.insert({
|
|
24
|
+
id: id,
|
|
25
|
+
name: name,
|
|
26
|
+
type: 'free',
|
|
27
|
+
slug: slugify(id),
|
|
28
|
+
created_at: knex.raw(`CURRENT_TIMESTAMP`)
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
async function down(knex) {
|
|
32
|
+
logging.info('Removing free tier');
|
|
33
|
+
await knex('products')
|
|
34
|
+
.where('type', 'free')
|
|
35
|
+
.del();
|
|
36
|
+
}
|
|
37
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
if (knex.client.config.client !== 'mysql') {
|
|
8
|
+
logging.warn('Skipping cleanup of duplicate offer redemptions - database is not MySQL');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
logging.info('Looking for duplicate offer redemptions.');
|
|
12
|
+
|
|
13
|
+
const duplicates = await knex('offer_redemptions')
|
|
14
|
+
.select('subscription_id')
|
|
15
|
+
.count('subscription_id as count')
|
|
16
|
+
.groupBy('subscription_id')
|
|
17
|
+
.having('count', '>', 1);
|
|
18
|
+
|
|
19
|
+
if (!duplicates.length) {
|
|
20
|
+
logging.info('No duplicate offer redemptions found.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
logging.info(`Found ${duplicates.length} duplicate offer redemptions.`);
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
27
|
+
for (const duplicate of duplicates) {
|
|
28
|
+
const offerRedemptions = await knex('offer_redemptions')
|
|
29
|
+
.select('id')
|
|
30
|
+
.where('subscription_id', duplicate.subscription_id);
|
|
31
|
+
|
|
32
|
+
const [offerRedemptionToKeep, ...offerRedemptionsToDelete] = offerRedemptions;
|
|
33
|
+
|
|
34
|
+
logging.info(`Keeping offer redemption ${offerRedemptionToKeep.id}`);
|
|
35
|
+
|
|
36
|
+
logging.info(`Deleting ${offerRedemptionsToDelete.length} duplicates`);
|
|
37
|
+
await knex('offer_redemptions')
|
|
38
|
+
.whereIn('id', offerRedemptionsToDelete.map(x => x.id))
|
|
39
|
+
.del();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async function down() {
|
|
43
|
+
logging.warn('Not recreating duplicate offer redemptions');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
);
|
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
{
|
|
4
4
|
"name": "Product",
|
|
5
5
|
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"name": "Free",
|
|
8
|
+
"slug": "free",
|
|
9
|
+
"type": "free",
|
|
10
|
+
"active": true
|
|
11
|
+
},
|
|
6
12
|
{
|
|
7
13
|
"name": "Default Product",
|
|
8
|
-
"slug": "default-product"
|
|
14
|
+
"slug": "default-product",
|
|
15
|
+
"type": "paid",
|
|
16
|
+
"active": true
|
|
9
17
|
}
|
|
10
18
|
]
|
|
11
19
|
},
|
|
@@ -378,9 +378,11 @@ module.exports = {
|
|
|
378
378
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
379
379
|
name: {type: 'string', maxlength: 191, nullable: false},
|
|
380
380
|
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
381
|
+
active: {type: 'boolean', nullable: false, defaultTo: true},
|
|
381
382
|
monthly_price_id: {type: 'string', maxlength: 24, nullable: true},
|
|
382
383
|
yearly_price_id: {type: 'string', maxlength: 24, nullable: true},
|
|
383
384
|
description: {type: 'string', maxlength: 191, nullable: true},
|
|
385
|
+
type: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'paid', validations: {isIn: [['paid', 'free']]}},
|
|
384
386
|
created_at: {type: 'dateTime', nullable: false},
|
|
385
387
|
updated_at: {type: 'dateTime', nullable: true}
|
|
386
388
|
},
|
|
@@ -75,8 +75,9 @@ module.exports = function (Bookshelf) {
|
|
|
75
75
|
fixBools: function fixBools(attrs) {
|
|
76
76
|
const self = this;
|
|
77
77
|
_.each(attrs, function each(value, key) {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const tableDef = schema.tables[self.tableName];
|
|
79
|
+
const columnDef = tableDef ? tableDef[key] : null;
|
|
80
|
+
if (columnDef && (columnDef.type === 'bool' || columnDef.type === 'boolean')) {
|
|
80
81
|
attrs[key] = value ? true : false;
|
|
81
82
|
}
|
|
82
83
|
});
|
|
@@ -100,6 +100,14 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
100
100
|
|
|
101
101
|
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
|
102
102
|
|
|
103
|
+
// Support tag creation with `posts: [{..., tags: [{slug: 'new'}]}]`
|
|
104
|
+
// In that situation we have a slug but no name so validation will fail
|
|
105
|
+
// unless we set one automatically. Re-using slug for name matches our
|
|
106
|
+
// opposite name->slug behaviour.
|
|
107
|
+
if (!newTag.get('name') && newTag.get('slug')) {
|
|
108
|
+
this.set('name', newTag.get('slug'));
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
// name: #later slug: hash-later
|
|
104
112
|
if (/^#/.test(newTag.get('name'))) {
|
|
105
113
|
this.set('visibility', 'internal');
|
|
@@ -173,21 +173,7 @@ function createApiInstance(config) {
|
|
|
173
173
|
stripe: config.getStripePaymentConfig()
|
|
174
174
|
},
|
|
175
175
|
models: {
|
|
176
|
-
|
|
177
|
-
* Settings do not have their own models, so we wrap the webhook in a "fake" model
|
|
178
|
-
*/
|
|
179
|
-
StripeWebhook: {
|
|
180
|
-
async upsert(data, options) {
|
|
181
|
-
const settings = [{
|
|
182
|
-
key: 'members_stripe_webhook_id',
|
|
183
|
-
value: data.webhook_id
|
|
184
|
-
}, {
|
|
185
|
-
key: 'members_stripe_webhook_secret',
|
|
186
|
-
value: data.secret
|
|
187
|
-
}];
|
|
188
|
-
await models.Settings.edit(settings, options);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
176
|
+
EmailRecipient: models.EmailRecipient,
|
|
191
177
|
StripeCustomer: models.MemberStripeCustomer,
|
|
192
178
|
StripeCustomerSubscription: models.StripeCustomerSubscription,
|
|
193
179
|
Member: models.Member,
|
|
@@ -137,8 +137,6 @@ class MembersConfigProvider {
|
|
|
137
137
|
getStripeUrlConfig() {
|
|
138
138
|
const siteUrl = this._urlUtils.getSiteUrl();
|
|
139
139
|
|
|
140
|
-
const webhookHandlerUrl = new URL('members/webhooks/stripe/', siteUrl);
|
|
141
|
-
|
|
142
140
|
const checkoutSuccessUrl = new URL(siteUrl);
|
|
143
141
|
checkoutSuccessUrl.searchParams.set('stripe', 'success');
|
|
144
142
|
const checkoutCancelUrl = new URL(siteUrl);
|
|
@@ -153,8 +151,7 @@ class MembersConfigProvider {
|
|
|
153
151
|
checkoutSuccess: checkoutSuccessUrl.href,
|
|
154
152
|
checkoutCancel: checkoutCancelUrl.href,
|
|
155
153
|
billingSuccess: billingSuccessUrl.href,
|
|
156
|
-
billingCancel: billingCancelUrl.href
|
|
157
|
-
webhookHandler: webhookHandlerUrl.href
|
|
154
|
+
billingCancel: billingCancelUrl.href
|
|
158
155
|
};
|
|
159
156
|
}
|
|
160
157
|
|
|
@@ -175,11 +172,6 @@ class MembersConfigProvider {
|
|
|
175
172
|
checkoutCancelUrl: urls.checkoutCancel,
|
|
176
173
|
billingSuccessUrl: urls.billingSuccess,
|
|
177
174
|
billingCancelUrl: urls.billingCancel,
|
|
178
|
-
webhookHandlerUrl: urls.webhookHandler,
|
|
179
|
-
webhook: {
|
|
180
|
-
id: this._settingsCache.get('members_stripe_webhook_id'),
|
|
181
|
-
secret: this._settingsCache.get('members_stripe_webhook_secret')
|
|
182
|
-
},
|
|
183
175
|
product: {
|
|
184
176
|
name: this._settingsCache.get('stripe_product_name')
|
|
185
177
|
},
|
|
@@ -109,10 +109,13 @@ const getPortalProductPrices = async function () {
|
|
|
109
109
|
monthlyPrice: product.monthlyPrice,
|
|
110
110
|
yearlyPrice: product.yearlyPrice,
|
|
111
111
|
benefits: product.benefits,
|
|
112
|
+
type: product.type,
|
|
112
113
|
prices: productPrices
|
|
113
114
|
};
|
|
114
115
|
});
|
|
115
|
-
const defaultProduct = products
|
|
116
|
+
const defaultProduct = products.find((product) => {
|
|
117
|
+
return product.type === 'paid';
|
|
118
|
+
});
|
|
116
119
|
const defaultPrices = defaultProduct ? defaultProduct.prices : [];
|
|
117
120
|
let portalProducts = defaultProduct ? [defaultProduct] : [];
|
|
118
121
|
if (labsService.isSet('multipleProducts')) {
|
|
@@ -234,6 +237,5 @@ module.exports = {
|
|
|
234
237
|
getOfferData,
|
|
235
238
|
updateMemberData,
|
|
236
239
|
getMemberSiteData,
|
|
237
|
-
deleteSession
|
|
238
|
-
stripeWebhooks: (req, res, next) => membersService.api.middleware.handleStripeWebhook(req, res, next)
|
|
240
|
+
deleteSession
|
|
239
241
|
};
|
|
@@ -5,7 +5,6 @@ const db = require('../../data/db');
|
|
|
5
5
|
const MembersConfigProvider = require('./config');
|
|
6
6
|
const MembersCSVImporter = require('@tryghost/members-importer');
|
|
7
7
|
const MembersStats = require('./stats/members-stats');
|
|
8
|
-
const createMembersApiInstance = require('./api');
|
|
9
8
|
const createMembersSettingsInstance = require('./settings');
|
|
10
9
|
const logging = require('@tryghost/logging');
|
|
11
10
|
const urlUtils = require('../../../shared/url-utils');
|
|
@@ -16,7 +15,6 @@ const models = require('../../models');
|
|
|
16
15
|
const _ = require('lodash');
|
|
17
16
|
const {GhostMailer} = require('../mail');
|
|
18
17
|
const jobsService = require('../jobs');
|
|
19
|
-
const stripeService = require('../stripe');
|
|
20
18
|
|
|
21
19
|
const messages = {
|
|
22
20
|
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
|
|
@@ -26,9 +24,6 @@ const messages = {
|
|
|
26
24
|
emailVerificationEmailMessage: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`
|
|
27
25
|
};
|
|
28
26
|
|
|
29
|
-
// Bind to settings.edited to update systems based on settings changes, similar to the bridge and models/base/listeners
|
|
30
|
-
const events = require('../../lib/common/events');
|
|
31
|
-
|
|
32
27
|
const ghostMailer = new GhostMailer();
|
|
33
28
|
|
|
34
29
|
const membersConfig = new MembersConfigProvider({
|
|
@@ -40,16 +35,6 @@ const membersConfig = new MembersConfigProvider({
|
|
|
40
35
|
let membersApi;
|
|
41
36
|
let membersSettings;
|
|
42
37
|
|
|
43
|
-
function reconfigureMembersAPI() {
|
|
44
|
-
const reconfiguredMembersAPI = createMembersApiInstance(membersConfig);
|
|
45
|
-
reconfiguredMembersAPI.bus.on('ready', function () {
|
|
46
|
-
membersApi = reconfiguredMembersAPI;
|
|
47
|
-
});
|
|
48
|
-
reconfiguredMembersAPI.bus.on('error', function (err) {
|
|
49
|
-
logging.error(err);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
38
|
/**
|
|
54
39
|
* @description Calculates threshold based on following formula
|
|
55
40
|
* Threshold = max{[current number of members], [volume threshold]}
|
|
@@ -57,7 +42,7 @@ function reconfigureMembersAPI() {
|
|
|
57
42
|
* @returns {Promise<number>}
|
|
58
43
|
*/
|
|
59
44
|
const fetchImportThreshold = async () => {
|
|
60
|
-
const membersTotal = await
|
|
45
|
+
const membersTotal = await module.exports.stats.getTotalMembers();
|
|
61
46
|
const configThreshold = _.get(config.get('hostSettings'), 'emailVerification.importThreshold');
|
|
62
47
|
const volumeThreshold = (configThreshold === undefined) ? Infinity : configThreshold;
|
|
63
48
|
const threshold = Math.max(membersTotal, volumeThreshold);
|
|
@@ -68,7 +53,7 @@ const fetchImportThreshold = async () => {
|
|
|
68
53
|
const membersImporter = new MembersCSVImporter({
|
|
69
54
|
storagePath: config.getContentPath('data'),
|
|
70
55
|
getTimezone: () => settingsCache.get('timezone'),
|
|
71
|
-
getMembersApi: () =>
|
|
56
|
+
getMembersApi: () => module.exports.api,
|
|
72
57
|
sendEmail: ghostMailer.send.bind(ghostMailer),
|
|
73
58
|
isSet: labsService.isSet.bind(labsService),
|
|
74
59
|
addJob: jobsService.addJob.bind(jobsService),
|
|
@@ -125,18 +110,14 @@ const processImport = async (options) => {
|
|
|
125
110
|
return result;
|
|
126
111
|
};
|
|
127
112
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const membersService = {
|
|
113
|
+
module.exports = {
|
|
131
114
|
async init() {
|
|
115
|
+
const stripeService = require('../stripe');
|
|
116
|
+
const createMembersApiInstance = require('./api');
|
|
132
117
|
const env = config.get('env');
|
|
133
118
|
|
|
119
|
+
// @TODO Move to stripe service
|
|
134
120
|
if (env !== 'production') {
|
|
135
|
-
if (!process.env.WEBHOOK_SECRET && stripeService.api.configured) {
|
|
136
|
-
process.env.WEBHOOK_SECRET = 'DEFAULT_WEBHOOK_SECRET';
|
|
137
|
-
logging.warn(tpl(messages.remoteWebhooksInDevelopment));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
121
|
if (stripeService.api.configured && stripeService.api.mode === 'live') {
|
|
141
122
|
throw new errors.IncorrectUsageError({
|
|
142
123
|
message: tpl(messages.noLiveKeysInDevelopment)
|
|
@@ -166,6 +147,12 @@ const membersService = {
|
|
|
166
147
|
logging.error(err);
|
|
167
148
|
}
|
|
168
149
|
})();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await stripeService.migrations.execute();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
logging.error(err);
|
|
155
|
+
}
|
|
169
156
|
},
|
|
170
157
|
contentGating: require('./content-gating'),
|
|
171
158
|
|
|
@@ -187,7 +174,7 @@ const membersService = {
|
|
|
187
174
|
cookieKeys: [settingsCache.get('theme_session_secret')],
|
|
188
175
|
cookieName: 'ghost-members-ssr',
|
|
189
176
|
cookieCacheName: 'ghost-members-ssr-cache',
|
|
190
|
-
getMembersApi: () =>
|
|
177
|
+
getMembersApi: () => module.exports.api
|
|
191
178
|
}),
|
|
192
179
|
|
|
193
180
|
stripeConnect: require('./stripe-connect'),
|
|
@@ -199,7 +186,6 @@ const membersService = {
|
|
|
199
186
|
settingsCache: settingsCache,
|
|
200
187
|
isSQLite: config.get('database:client') === 'sqlite3'
|
|
201
188
|
})
|
|
202
|
-
};
|
|
203
189
|
|
|
204
|
-
|
|
190
|
+
};
|
|
205
191
|
module.exports.middleware = require('./middleware');
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
|
|
2
2
|
const OffersModule = require('@tryghost/members-offers');
|
|
3
3
|
|
|
4
|
-
const stripeService = require('../stripe');
|
|
5
|
-
|
|
6
4
|
const config = require('../../../shared/config');
|
|
7
5
|
const urlUtils = require('../../../shared/url-utils');
|
|
8
6
|
const models = require('../../models');
|
|
@@ -19,8 +17,7 @@ module.exports = {
|
|
|
19
17
|
const offersModule = OffersModule.create({
|
|
20
18
|
OfferModel: models.Offer,
|
|
21
19
|
OfferRedemptionModel: models.OfferRedemption,
|
|
22
|
-
redirectManager: redirectManager
|
|
23
|
-
stripeAPIService: stripeService.api
|
|
20
|
+
redirectManager: redirectManager
|
|
24
21
|
});
|
|
25
22
|
|
|
26
23
|
this.api = offersModule.api;
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
const {isPlainObject} = require('lodash');
|
|
2
2
|
const config = require('../../../shared/config');
|
|
3
3
|
const labs = require('../../../shared/labs');
|
|
4
|
+
const databaseInfo = require('../../data/db/info');
|
|
4
5
|
const ghostVersion = require('@tryghost/version');
|
|
5
6
|
|
|
6
7
|
module.exports = function getConfigProperties() {
|
|
7
8
|
const configProperties = {
|
|
8
9
|
version: ghostVersion.full,
|
|
9
10
|
environment: config.get('env'),
|
|
10
|
-
database:
|
|
11
|
+
database: databaseInfo.getEngine(),
|
|
11
12
|
mail: isPlainObject(config.get('mail')) ? config.get('mail').transport : '',
|
|
12
13
|
useGravatar: !config.isPrivacyDisabled('useGravatar'),
|
|
13
14
|
labs: labs.getAll(),
|
|
14
15
|
clientExtensions: config.get('clientExtensions') || {},
|
|
15
16
|
enableDeveloperExperiments: config.get('enableDeveloperExperiments') || false,
|
|
16
17
|
stripeDirect: config.get('stripeDirect'),
|
|
17
|
-
mailgunIsConfigured: config.get('bulkEmail') && config.get('bulkEmail').mailgun,
|
|
18
|
+
mailgunIsConfigured: !!(config.get('bulkEmail') && config.get('bulkEmail').mailgun),
|
|
18
19
|
emailAnalytics: config.get('emailAnalytics'),
|
|
19
20
|
hostSettings: config.get('hostSettings'),
|
|
20
21
|
tenor: config.get('tenor')
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
const
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const tpl = require('@tryghost/tpl');
|
|
2
3
|
|
|
4
|
+
const messages = {
|
|
5
|
+
remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// @TODO Refactor to a class w/ constructor
|
|
3
9
|
module.exports = {
|
|
4
|
-
getConfig(settings, config) {
|
|
10
|
+
getConfig(settings, config, urlUtils) {
|
|
5
11
|
/**
|
|
6
12
|
* @param {'direct' | 'connect'} type - The "type" of keys to fetch from settings
|
|
7
13
|
* @returns {{publicKey: string, secretKey: string} | null}
|
|
@@ -42,16 +48,25 @@ module.exports = {
|
|
|
42
48
|
if (!keys) {
|
|
43
49
|
return null;
|
|
44
50
|
}
|
|
51
|
+
|
|
52
|
+
const env = config.get('env');
|
|
53
|
+
let webhookSecret = process.env.WEBHOOK_SECRET;
|
|
54
|
+
|
|
55
|
+
if (env !== 'production') {
|
|
56
|
+
if (!webhookSecret) {
|
|
57
|
+
webhookSecret = 'DEFAULT_WEBHOOK_SECRET';
|
|
58
|
+
logging.warn(tpl(messages.remoteWebhooksInDevelopment));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const webhookHandlerUrl = new URL('members/webhooks/stripe/', urlUtils.getSiteUrl());
|
|
63
|
+
|
|
45
64
|
return {
|
|
46
65
|
secretKey: keys.secretKey,
|
|
47
66
|
publicKey: keys.publicKey,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
version: ghostVersion.original,
|
|
52
|
-
url: 'https://ghost.org/'
|
|
53
|
-
},
|
|
54
|
-
enablePromoCodes: config.get('enableStripePromoCodes')
|
|
67
|
+
enablePromoCodes: config.get('enableStripePromoCodes'),
|
|
68
|
+
webhookSecret: webhookSecret,
|
|
69
|
+
webhookHandlerUrl: webhookHandlerUrl.href
|
|
55
70
|
};
|
|
56
71
|
}
|
|
57
72
|
};
|
|
@@ -1,45 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const StripeAPIService = require('@tryghost/members-stripe-service');
|
|
3
|
-
|
|
4
|
-
const config = require('../../../shared/config');
|
|
5
|
-
const settings = require('../../../shared/settings-cache');
|
|
6
|
-
const events = require('../../lib/common/events');
|
|
7
|
-
|
|
8
|
-
const {getConfig} = require('./config');
|
|
9
|
-
|
|
10
|
-
const api = new StripeAPIService({
|
|
11
|
-
config: {}
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const stripeKeySettings = [
|
|
15
|
-
'stripe_publishable_key',
|
|
16
|
-
'stripe_secret_key',
|
|
17
|
-
'stripe_connect_publishable_key',
|
|
18
|
-
'stripe_connect_secret_key'
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function configureApi() {
|
|
22
|
-
const cfg = getConfig(settings, config);
|
|
23
|
-
if (cfg) {
|
|
24
|
-
api.configure(cfg);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const debouncedConfigureApi = _.debounce(() => {
|
|
29
|
-
configureApi();
|
|
30
|
-
events.emit('services.stripe.reconfigured');
|
|
31
|
-
}, 600);
|
|
32
|
-
|
|
33
|
-
module.exports = {
|
|
34
|
-
async init() {
|
|
35
|
-
configureApi();
|
|
36
|
-
events.on('settings.edited', function (model) {
|
|
37
|
-
if (!stripeKeySettings.includes(model.get('key'))) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
debouncedConfigureApi();
|
|
41
|
-
});
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
api
|
|
45
|
-
};
|
|
1
|
+
module.exports = require('./service');
|