ghost 4.32.0 → 4.33.0

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 (80) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/core/boot.js +3 -0
  4. package/core/built/assets/{chunk.3.8f95b516d88ff4eec64c.js → chunk.3.4906cf0b01d6d8e33374.js} +134 -130
  5. package/core/built/assets/{ghost-dark-43f5faa616791819b3ae91e128ec41f0.css → ghost-dark-661a50922267648a0362c3d367a22013.css} +1 -1
  6. package/core/built/assets/{ghost.min-c3f7cbabcc1a69476534453c6c747ee3.css → ghost.min-1f0218f33e08f8d69b2159977d0c9318.css} +1 -1
  7. package/core/built/assets/{ghost.min-2b20489c79323b165909749382adc158.js → ghost.min-501554f903f29164473a5dc620caaddb.js} +719 -726
  8. package/core/built/assets/img/apple-touch-icon-74680e326a7e87b159d366c7d4fb3d4b.png +0 -0
  9. package/core/built/assets/img/large-ac90af7c93a4b47e8d956fa9fef31d9d.png +0 -0
  10. package/core/built/assets/img/medium-fef07013cffd5c45a655a250912a0ad7.png +0 -0
  11. package/core/built/assets/img/small-b90396925485f17b2ca82c31be42de5f.png +0 -0
  12. package/core/built/assets/img/touch-icon-ipad-2e78629d62ad05746f980f14623dfadb.png +0 -0
  13. package/core/built/assets/img/touch-icon-iphone-93ed4382d391be9180093fd77ce8f410.png +0 -0
  14. package/core/built/assets/{vendor.min-987af30228885bce50f05c4723fe6f53.css → vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css} +1 -1
  15. package/core/built/assets/{vendor.min-992a9b07f7d0a67b5a4afd91319edf8b.js → vendor.min-d43620e98444a46441495445f4c155f8.js} +1407 -1455
  16. package/core/frontend/apps/amp/lib/views/amp.hbs +4 -4
  17. package/core/frontend/helpers/date.js +3 -4
  18. package/core/frontend/meta/description.js +3 -3
  19. package/core/frontend/services/routing/config/canary.js +1 -1
  20. package/core/frontend/services/routing/config/v4.js +1 -1
  21. package/core/frontend/services/sitemap/base-generator.js +21 -18
  22. package/core/frontend/services/sitemap/handler.js +13 -4
  23. package/core/frontend/services/sitemap/index-generator.js +20 -10
  24. package/core/frontend/services/sitemap/manager.js +8 -5
  25. package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +3 -1
  26. package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +1 -6
  27. package/core/frontend/src/cards/css/audio.css +5 -0
  28. package/core/frontend/src/cards/css/bookmark.css +5 -0
  29. package/core/frontend/src/cards/css/button.css +5 -0
  30. package/core/frontend/src/cards/css/callout.css +5 -0
  31. package/core/frontend/src/cards/css/file.css +6 -1
  32. package/core/frontend/src/cards/css/gallery.css +5 -0
  33. package/core/frontend/src/cards/css/header.css +5 -0
  34. package/core/frontend/src/cards/css/nft.css +5 -0
  35. package/core/frontend/src/cards/css/product.css +5 -0
  36. package/core/frontend/src/cards/css/toggle.css +5 -0
  37. package/core/frontend/src/cards/css/video.css +4 -0
  38. package/core/frontend/views/unsubscribe.hbs +12 -7
  39. package/core/frontend/web/site.js +7 -4
  40. package/core/server/api/canary/settings.js +2 -1
  41. package/core/server/api/canary/utils/serializers/output/products.js +4 -0
  42. package/core/server/data/db/info.js +4 -0
  43. package/core/server/data/migrations/versions/4.33/2022-01-14-11-50-add-type-column-to-products.js +12 -0
  44. package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +37 -0
  45. package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +46 -0
  46. package/core/server/data/migrations/versions/4.33/2022-01-19-10-43-add-active-column-to-products-table.js +7 -0
  47. package/core/server/data/schema/default-settings.json +1 -1
  48. package/core/server/data/schema/fixtures/fixtures.json +9 -1
  49. package/core/server/data/schema/schema.js +2 -0
  50. package/core/server/models/base/plugins/data-manipulation.js +3 -2
  51. package/core/server/models/product.js +4 -0
  52. package/core/server/models/single-use-token.js +1 -1
  53. package/core/server/models/tag.js +8 -0
  54. package/core/server/services/mega/template.js +4 -2
  55. package/core/server/services/members/api.js +2 -16
  56. package/core/server/services/members/config.js +1 -9
  57. package/core/server/services/members/middleware.js +5 -3
  58. package/core/server/services/members/service.js +19 -46
  59. package/core/server/services/offers/service.js +1 -4
  60. package/core/server/services/public-config/config.js +3 -2
  61. package/core/server/services/stripe/config.js +24 -9
  62. package/core/server/services/stripe/index.js +36 -28
  63. package/core/server/services/themes/activation-bridge.js +3 -10
  64. package/core/server/services/themes/index.js +0 -21
  65. package/core/server/services/twitter-embed.js +1 -2
  66. package/core/server/update-check.js +2 -1
  67. package/core/server/web/admin/views/default-prod.html +10 -13
  68. package/core/server/web/admin/views/default.html +10 -13
  69. package/core/server/web/api/canary/admin/routes.js +2 -6
  70. package/core/server/web/members/app.js +3 -2
  71. package/core/server/web/shared/middleware/cache-control.js +12 -0
  72. package/core/shared/config/defaults.json +2 -2
  73. package/core/shared/labs.js +2 -14
  74. package/package.json +71 -69
  75. package/yarn.lock +2577 -2997
  76. package/core/built/assets/img/large-bf46e150380a4979a7389b45f5bb479d.png +0 -0
  77. package/core/built/assets/img/medium-7359075af28d69523987ff4c0e2067c5.png +0 -0
  78. package/core/built/assets/img/small-42ff134f320b8b5a6eca3781c4e4b2db.png +0 -0
  79. package/core/built/assets/img/touch-icon-ipad-3117c0fa950d0fc43c95becef61f4167.png +0 -0
  80. package/core/built/assets/img/touch-icon-iphone-d2790931c3477664981061ed9fa5242e.png +0 -0
@@ -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: {
@@ -33,7 +33,7 @@ const SingleUseToken = ghostBookshelf.Model.extend({
33
33
  } catch (err) {
34
34
  logging.error(err);
35
35
  }
36
- }, 10000);
36
+ }, 10 * 60 * 1000);
37
37
  }
38
38
 
39
39
  return model;
@@ -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');
@@ -407,6 +407,10 @@ figure blockquote p {
407
407
  text-decoration: underline;
408
408
  }
409
409
 
410
+ a[data-flickr-embed] img {
411
+ height: auto;
412
+ }
413
+
410
414
  .kg-bookmark-card {
411
415
  width: 100%;
412
416
  background: #ffffff;
@@ -1106,8 +1110,6 @@ ${ templateSettings.showBadge ? `
1106
1110
  }
1107
1111
  ` : ''}
1108
1112
 
1109
- /* ----- ENDIF THE BROWSER ----- */
1110
-
1111
1113
  </style>
1112
1114
  </head>
1113
1115
 
@@ -25,7 +25,7 @@ function createApiInstance(config) {
25
25
  tokenConfig: config.getTokenConfig(),
26
26
  auth: {
27
27
  getSigninURL: config.getSigninURL.bind(config),
28
- allowSelfSignup: config.getAllowSelfSignup(),
28
+ allowSelfSignup: config.getAllowSelfSignup.bind(config),
29
29
  tokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY)
30
30
  },
31
31
  mail: {
@@ -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,36 +110,14 @@ const processImport = async (options) => {
125
110
  return result;
126
111
  };
127
112
 
128
- const debouncedReconfigureMembersAPI = _.debounce(reconfigureMembersAPI, 600);
129
-
130
- // Bind to events to automatically keep subscription info up-to-date from settings
131
- events.on('settings.edited', function updateSettingFromModel(settingModel) {
132
- if (![
133
- 'members_signup_access',
134
- 'members_from_address',
135
- 'members_support_address',
136
- 'members_reply_address',
137
- 'stripe_product_name',
138
- 'stripe_plans'
139
- ].includes(settingModel.get('key'))) {
140
- return;
141
- }
142
-
143
- debouncedReconfigureMembersAPI();
144
- });
145
-
146
- events.on('services.stripe.reconfigured', reconfigureMembersAPI);
147
-
148
- const membersService = {
113
+ module.exports = {
149
114
  async init() {
115
+ const stripeService = require('../stripe');
116
+ const createMembersApiInstance = require('./api');
150
117
  const env = config.get('env');
151
118
 
119
+ // @TODO Move to stripe service
152
120
  if (env !== 'production') {
153
- if (!process.env.WEBHOOK_SECRET && stripeService.api.configured) {
154
- process.env.WEBHOOK_SECRET = 'DEFAULT_WEBHOOK_SECRET';
155
- logging.warn(tpl(messages.remoteWebhooksInDevelopment));
156
- }
157
-
158
121
  if (stripeService.api.configured && stripeService.api.mode === 'live') {
159
122
  throw new errors.IncorrectUsageError({
160
123
  message: tpl(messages.noLiveKeysInDevelopment)
@@ -175,6 +138,17 @@ const membersService = {
175
138
  logging.error(err);
176
139
  });
177
140
  }
141
+
142
+ (async () => {
143
+ try {
144
+ const collection = await models.SingleUseToken.fetchAll();
145
+ await collection.invokeThen('destroy');
146
+ } catch (err) {
147
+ logging.error(err);
148
+ }
149
+ })();
150
+
151
+ await stripeService.migrations.execute();
178
152
  },
179
153
  contentGating: require('./content-gating'),
180
154
 
@@ -196,7 +170,7 @@ const membersService = {
196
170
  cookieKeys: [settingsCache.get('theme_session_secret')],
197
171
  cookieName: 'ghost-members-ssr',
198
172
  cookieCacheName: 'ghost-members-ssr-cache',
199
- getMembersApi: () => membersService.api
173
+ getMembersApi: () => module.exports.api
200
174
  }),
201
175
 
202
176
  stripeConnect: require('./stripe-connect'),
@@ -208,7 +182,6 @@ const membersService = {
208
182
  settingsCache: settingsCache,
209
183
  isSQLite: config.get('database:client') === 'sqlite3'
210
184
  })
211
- };
212
185
 
213
- module.exports = membersService;
186
+ };
214
187
  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,53 @@
1
1
  const _ = require('lodash');
2
- const StripeAPIService = require('@tryghost/members-stripe-service');
3
-
2
+ const StripeService = require('@tryghost/members-stripe-service');
3
+ const membersService = require('../members');
4
4
  const config = require('../../../shared/config');
5
5
  const settings = require('../../../shared/settings-cache');
6
+ const urlUtils = require('../../../shared/url-utils');
6
7
  const events = require('../../lib/common/events');
7
-
8
+ const models = require('../../models');
8
9
  const {getConfig} = require('./config');
9
10
 
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
11
  function configureApi() {
22
- const cfg = getConfig(settings, config);
12
+ const cfg = getConfig(settings, config, urlUtils);
23
13
  if (cfg) {
24
- api.configure(cfg);
14
+ module.exports.configure(cfg);
15
+ return true;
25
16
  }
17
+ return false;
26
18
  }
27
19
 
28
20
  const debouncedConfigureApi = _.debounce(() => {
29
21
  configureApi();
30
- events.emit('services.stripe.reconfigured');
31
22
  }, 600);
32
23
 
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
- },
24
+ module.exports = new StripeService({
25
+ membersService,
26
+ models: _.pick(models, ['Product', 'StripePrice', 'StripeCustomerSubscription', 'StripeProduct', 'MemberStripeCustomer', 'Offer', 'Settings']),
27
+ StripeWebhook: {
28
+ async get() {
29
+ return {
30
+ webhook_id: settings.get('members_stripe_webhook_id'),
31
+ secret: settings.get('members_stripe_webhook_secret')
32
+ };
33
+ },
34
+ async save(data) {
35
+ await models.Settings.edit([{
36
+ key: 'members_stripe_webhook_id',
37
+ value: data.webhook_id
38
+ }, {
39
+ key: 'members_stripe_webhook_secret',
40
+ value: data.secret
41
+ }]);
42
+ }
43
+ }
44
+ });
43
45
 
44
- api
46
+ module.exports.init = async function init() {
47
+ configureApi();
48
+ events.on('settings.edited', function (model) {
49
+ if (['stripe_publishable_key', 'stripe_secret_key', 'stripe_connect_publishable_key', 'stripe_connect_secret_key'].includes(model.get('key'))) {
50
+ debouncedConfigureApi();
51
+ }
52
+ });
45
53
  };
@@ -1,6 +1,5 @@
1
1
  const debug = require('@tryghost/debug')('themes');
2
2
  const bridge = require('../../../bridge');
3
- const labs = require('../../../shared/labs');
4
3
  const customThemeSettings = require('../custom-theme-settings');
5
4
 
6
5
  /**
@@ -11,25 +10,19 @@ module.exports = {
11
10
  activateFromBoot: async (themeName, theme, checkedTheme) => {
12
11
  debug('Activating theme (method A on boot)', themeName);
13
12
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
14
- if (labs.isSet('customThemeSettings')) {
15
- await customThemeSettings.api.activateTheme(themeName, checkedTheme);
16
- }
13
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
17
14
  await bridge.activateTheme(theme, checkedTheme);
18
15
  },
19
16
  activateFromAPI: async (themeName, theme, checkedTheme) => {
20
17
  debug('Activating theme (method B on API "activate")', themeName);
21
18
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
22
- if (labs.isSet('customThemeSettings')) {
23
- await customThemeSettings.api.activateTheme(themeName, checkedTheme);
24
- }
19
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
25
20
  await bridge.activateTheme(theme, checkedTheme);
26
21
  },
27
22
  activateFromAPIOverride: async (themeName, theme, checkedTheme) => {
28
23
  debug('Activating theme (method C on API "override")', themeName);
29
24
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
30
- if (labs.isSet('customThemeSettings')) {
31
- await customThemeSettings.api.activateTheme(themeName, checkedTheme);
32
- }
25
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
33
26
  await bridge.activateTheme(theme, checkedTheme);
34
27
  }
35
28
  };
@@ -6,12 +6,6 @@ const installer = require('./installer');
6
6
 
7
7
  const settingsCache = require('../../../shared/settings-cache');
8
8
 
9
- // Needed for theme re-activation after customThemeSettings flag is toggled
10
- // @TODO: remove when customThemeSettings flag is removed
11
- const labs = require('../../../shared/labs');
12
- const events = require('../../lib/common/events');
13
- let _lastLabsValue;
14
-
15
9
  module.exports = {
16
10
  /*
17
11
  * Load the currently active theme
@@ -19,21 +13,6 @@ module.exports = {
19
13
  init: async () => {
20
14
  const themeName = settingsCache.get('active_theme');
21
15
 
22
- /**
23
- * When customThemeSettings labs flag is toggled we need to re-validate and activate
24
- * the active theme so that it's settings are read and synced
25
- *
26
- * @TODO: remove when customThemeSettings labs flag is removed
27
- */
28
- _lastLabsValue = labs.isSet('customThemeSettings');
29
- events.on('settings.labs.edited', () => {
30
- if (labs.isSet('customThemeSettings') !== _lastLabsValue) {
31
- _lastLabsValue = labs.isSet('customThemeSettings');
32
-
33
- activate.activate(settingsCache.get('active_theme'));
34
- }
35
- });
36
-
37
16
  return activate.loadAndActivate(themeName);
38
17
  },
39
18
  /**
@@ -1,6 +1,5 @@
1
1
  const {extract} = require('oembed-parser');
2
2
  const logging = require('@tryghost/logging');
3
- const labs = require('../../shared/labs');
4
3
 
5
4
  /**
6
5
  * @typedef {import('./oembed').ICustomProvider} ICustomProvider
@@ -43,7 +42,7 @@ class TwitterOEmbedProvider {
43
42
  /** @type {object} */
44
43
  const oembedData = await extract(url.href);
45
44
 
46
- if (this.dependencies.config.bearerToken && labs.isSet('richTwitterNewsletters')) {
45
+ if (this.dependencies.config.bearerToken) {
47
46
  const query = {
48
47
  expansions: ['attachments.poll_ids', 'attachments.media_keys', 'author_id', 'entities.mentions.username', 'geo.place_id', 'in_reply_to_user_id', 'referenced_tweets.id', 'referenced_tweets.id.author_id'],
49
48
  'media.fields': ['duration_ms', 'height', 'media_key', 'preview_image_url', 'type', 'url', 'width', 'public_metrics', 'alt_text'],