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.
Files changed (52) hide show
  1. package/core/boot.js +3 -0
  2. package/core/built/assets/{ghost-dark-67f6ba8347be37f997b3a7e430b29f72.css → ghost-dark-661a50922267648a0362c3d367a22013.css} +1 -1
  3. package/core/built/assets/{ghost.min-2d5f48403647d1e11805691cc7ad0835.css → ghost.min-1f0218f33e08f8d69b2159977d0c9318.css} +1 -1
  4. package/core/built/assets/{ghost.min-1f04d5d6f5f8f3739a86d5bc807cbe1c.js → ghost.min-501554f903f29164473a5dc620caaddb.js} +1285 -1229
  5. package/core/built/assets/img/apple-touch-icon-74680e326a7e87b159d366c7d4fb3d4b.png +0 -0
  6. package/core/built/assets/img/large-ac90af7c93a4b47e8d956fa9fef31d9d.png +0 -0
  7. package/core/built/assets/img/medium-fef07013cffd5c45a655a250912a0ad7.png +0 -0
  8. package/core/built/assets/img/small-b90396925485f17b2ca82c31be42de5f.png +0 -0
  9. package/core/built/assets/img/touch-icon-ipad-2e78629d62ad05746f980f14623dfadb.png +0 -0
  10. package/core/built/assets/img/touch-icon-iphone-93ed4382d391be9180093fd77ce8f410.png +0 -0
  11. package/core/built/assets/{vendor.min-df1ba725edbc456aa45af07d3443403e.js → vendor.min-d43620e98444a46441495445f4c155f8.js} +686 -731
  12. package/core/frontend/helpers/date.js +3 -4
  13. package/core/frontend/services/routing/config/canary.js +1 -1
  14. package/core/frontend/services/routing/config/v4.js +1 -1
  15. package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +3 -1
  16. package/core/frontend/web/site.js +5 -1
  17. package/core/server/api/canary/settings.js +2 -1
  18. package/core/server/api/canary/utils/serializers/output/products.js +4 -0
  19. package/core/server/data/db/info.js +4 -0
  20. package/core/server/data/migrations/versions/4.33/2022-01-14-11-50-add-type-column-to-products.js +12 -0
  21. package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +37 -0
  22. package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +46 -0
  23. package/core/server/data/migrations/versions/4.33/2022-01-19-10-43-add-active-column-to-products-table.js +7 -0
  24. package/core/server/data/schema/default-settings.json +1 -1
  25. package/core/server/data/schema/fixtures/fixtures.json +9 -1
  26. package/core/server/data/schema/schema.js +2 -0
  27. package/core/server/models/base/plugins/data-manipulation.js +3 -2
  28. package/core/server/models/product.js +4 -0
  29. package/core/server/models/tag.js +8 -0
  30. package/core/server/services/members/api.js +1 -15
  31. package/core/server/services/members/config.js +1 -9
  32. package/core/server/services/members/middleware.js +5 -3
  33. package/core/server/services/members/service.js +14 -28
  34. package/core/server/services/offers/service.js +1 -4
  35. package/core/server/services/public-config/config.js +3 -2
  36. package/core/server/services/stripe/config.js +24 -9
  37. package/core/server/services/stripe/index.js +1 -45
  38. package/core/server/services/stripe/service.js +58 -0
  39. package/core/server/update-check.js +2 -1
  40. package/core/server/web/admin/views/default-prod.html +9 -12
  41. package/core/server/web/admin/views/default.html +9 -12
  42. package/core/server/web/members/app.js +3 -2
  43. package/core/server/web/shared/middleware/cache-control.js +12 -0
  44. package/core/shared/config/defaults.json +2 -2
  45. package/core/shared/labs.js +2 -1
  46. package/package.json +59 -57
  47. package/yarn.lock +652 -749
  48. package/core/built/assets/img/large-bf46e150380a4979a7389b45f5bb479d.png +0 -0
  49. package/core/built/assets/img/medium-7359075af28d69523987ff4c0e2067c5.png +0 -0
  50. package/core/built/assets/img/small-42ff134f320b8b5a6eca3781c4e4b2db.png +0 -0
  51. package/core/built/assets/img/touch-icon-ipad-3117c0fa950d0fc43c95becef61f4167.png +0 -0
  52. 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);
@@ -54,7 +54,7 @@ module.exports.TAXONOMIES = {
54
54
  },
55
55
  author: {
56
56
  filter: 'authors:\'%s\'',
57
- editRedirect: '#/staff/:slug/',
57
+ editRedirect: '#/settings/staff/:slug/',
58
58
  resource: 'authors'
59
59
  }
60
60
  };
@@ -54,7 +54,7 @@ module.exports.TAXONOMIES = {
54
54
  },
55
55
  author: {
56
56
  filter: 'authors:\'%s\'',
57
- editRedirect: '#/staff/:slug/',
57
+ editRedirect: '#/settings/staff/:slug/',
58
58
  resource: 'authors'
59
59
  }
60
60
  };
@@ -28,7 +28,9 @@ function calculateLegacyPriceData(products) {
28
28
  };
29
29
  }
30
30
 
31
- const defaultProduct = products[0] || {};
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('/members/.well-known', (req, res, next) => membersService.api.middleware.wellKnown(req, res, next));
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 membersService.api.disconnectStripe();
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,4 @@
1
+ const connection = require('./connection');
2
+ const DatabaseInfo = require('@tryghost/database-info');
3
+
4
+ module.exports = new DatabaseInfo(connection);
@@ -0,0 +1,12 @@
1
+ const utils = require('../../utils');
2
+
3
+ module.exports = utils.createAddColumnMigration(
4
+ 'products',
5
+ 'type',
6
+ {
7
+ type: 'string',
8
+ maxlength: 50,
9
+ nullable: false,
10
+ defaultTo: 'paid'
11
+ }
12
+ );
@@ -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
+ );
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('products', 'active', {
4
+ type: 'boolean',
5
+ nullable: false,
6
+ defaultTo: true
7
+ });
@@ -397,7 +397,7 @@
397
397
  },
398
398
  "amp": {
399
399
  "amp": {
400
- "defaultValue": "true",
400
+ "defaultValue": "false",
401
401
  "validations": {
402
402
  "isIn": [["true", "false"]]
403
403
  },
@@ -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
- if (Object.prototype.hasOwnProperty.call(schema.tables[self.tableName], key)
79
- && schema.tables[self.tableName][key].type === 'bool') {
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
  });
@@ -4,6 +4,10 @@ const _ = require('lodash');
4
4
  const Product = ghostBookshelf.Model.extend({
5
5
  tableName: 'products',
6
6
 
7
+ defaults: {
8
+ active: true
9
+ },
10
+
7
11
  relationships: ['benefits'],
8
12
 
9
13
  relationshipBelongsTo: {
@@ -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[0];
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 membersService.stats.getTotalMembers();
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: () => membersService.api,
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
- events.on('services.stripe.reconfigured', reconfigureMembersAPI);
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: () => membersService.api
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
- module.exports = membersService;
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: config.get('database').client,
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 ghostVersion = require('@tryghost/version');
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
- appInfo: {
49
- name: 'Ghost',
50
- partner_id: 'pp_partner_DKmRVtTs4j9pwZ',
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
- const _ = require('lodash');
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');