ghost 4.20.4 → 4.22.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 (107) hide show
  1. package/.eslintrc.js +7 -1
  2. package/Gruntfile.js +2 -0
  3. package/content/themes/casper/assets/built/screen.css +1 -1
  4. package/content/themes/casper/assets/built/screen.css.map +1 -1
  5. package/content/themes/casper/assets/css/screen.css +263 -50
  6. package/content/themes/casper/default.hbs +12 -3
  7. package/content/themes/casper/index.hbs +25 -23
  8. package/content/themes/casper/package.json +91 -2
  9. package/content/themes/casper/partials/post-card.hbs +1 -1
  10. package/content/themes/casper/post.hbs +18 -14
  11. package/content/themes/casper/yarn.lock +245 -192
  12. package/core/boot.js +8 -0
  13. package/core/bridge.js +14 -0
  14. package/core/built/assets/{chunk.3.777d43e2ce954ba8b2f5.js → chunk.3.324fd0cc598c73650219.js} +59 -59
  15. package/core/built/assets/{ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css → ghost-dark-39fb496d051565531062d7e047d1c0b1.css} +1 -1
  16. package/core/built/assets/{ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css → ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css} +1 -1
  17. package/core/built/assets/{ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js → ghost.min-7da921f6c6cac3fe10da1ba104575440.js} +1802 -1911
  18. package/core/built/assets/{vendor.min-af502ac4142871500fc424f6a5a254ec.js → vendor.min-413f887176a041e6dbf88214ca9a7481.js} +7039 -6879
  19. package/core/frontend/apps/amp/lib/helpers/amp_content.js +16 -4
  20. package/core/frontend/helpers/asset.js +9 -1
  21. package/core/frontend/helpers/ghost_head.js +13 -1
  22. package/core/frontend/meta/title.js +15 -5
  23. package/core/frontend/services/card-assets/index.js +16 -0
  24. package/core/frontend/services/card-assets/service.js +101 -0
  25. package/core/frontend/services/theme-engine/config/defaults.json +4 -1
  26. package/core/frontend/services/theme-engine/config/index.js +1 -1
  27. package/core/frontend/src/cards/css/bookmark.css +83 -0
  28. package/core/frontend/src/cards/css/button.css +30 -0
  29. package/core/frontend/src/cards/css/callout.css +12 -0
  30. package/core/frontend/src/cards/css/gallery.css +36 -0
  31. package/core/frontend/src/cards/css/nft.css +85 -0
  32. package/core/frontend/src/cards/js/gallery.js +8 -0
  33. package/core/frontend/web/middleware/serve-public-file.js +10 -1
  34. package/core/frontend/web/routes.js +0 -1
  35. package/core/frontend/web/site.js +13 -9
  36. package/core/server/adapters/storage/LocalFilesStorage.js +17 -0
  37. package/core/server/adapters/storage/LocalImagesStorage.js +51 -0
  38. package/core/server/adapters/storage/LocalMediaStorage.js +24 -0
  39. package/core/server/adapters/storage/{LocalFileStorage.js → LocalStorageBase.js} +64 -51
  40. package/core/server/adapters/storage/index.js +1 -1
  41. package/core/server/adapters/storage/utils.js +2 -2
  42. package/core/server/api/canary/files.js +19 -0
  43. package/core/server/api/canary/index.js +8 -0
  44. package/core/server/api/canary/media.js +42 -0
  45. package/core/server/api/canary/members.js +6 -103
  46. package/core/server/api/canary/membersStripeConnect.js +0 -10
  47. package/core/server/api/canary/oembed.js +3 -0
  48. package/core/server/api/canary/redirects.js +1 -6
  49. package/core/server/api/canary/utils/serializers/input/index.js +4 -0
  50. package/core/server/api/canary/utils/serializers/input/media.js +8 -0
  51. package/core/server/api/canary/utils/serializers/input/pages.js +8 -0
  52. package/core/server/api/canary/utils/serializers/output/config.js +21 -14
  53. package/core/server/api/canary/utils/serializers/output/files.js +27 -0
  54. package/core/server/api/canary/utils/serializers/output/images.js +4 -0
  55. package/core/server/api/canary/utils/serializers/output/index.js +8 -0
  56. package/core/server/api/canary/utils/serializers/output/media.js +37 -0
  57. package/core/server/api/canary/utils/validators/input/files.js +7 -0
  58. package/core/server/api/canary/utils/validators/input/index.js +8 -0
  59. package/core/server/api/canary/utils/validators/input/media.js +11 -0
  60. package/core/server/api/v2/redirects.js +1 -6
  61. package/core/server/api/v2/utils/serializers/output/images.js +4 -0
  62. package/core/server/api/v3/members.js +5 -1
  63. package/core/server/api/v3/redirects.js +1 -6
  64. package/core/server/api/v3/utils/serializers/output/images.js +4 -0
  65. package/core/server/data/migrations/utils.js +55 -16
  66. package/core/server/data/migrations/versions/4.22/01-add-is-launch-complete-setting.js +8 -0
  67. package/core/server/data/migrations/versions/4.22/02-update-launch-complete-setting-from-user-data.js +39 -0
  68. package/core/server/data/schema/default-settings.json +8 -0
  69. package/core/server/frontend/ghost.min.css +1 -1
  70. package/core/server/lib/image/blog-icon.js +2 -4
  71. package/core/server/lib/image/image-size.js +1 -1
  72. package/core/server/services/limits.js +3 -6
  73. package/core/server/services/mega/template.js +62 -1
  74. package/core/server/services/members/api.js +1 -1
  75. package/core/server/services/members/emails/signup.js +2 -2
  76. package/core/server/services/members/stripe-connect.js +14 -0
  77. package/core/server/services/nft-oembed.js +71 -0
  78. package/core/server/services/oembed.js +145 -110
  79. package/core/server/services/offers/service.js +1 -31
  80. package/core/server/services/public-config/config.js +2 -1
  81. package/core/server/services/redirects/api.js +270 -0
  82. package/core/server/services/redirects/index.js +27 -12
  83. package/core/server/services/stripe/index.js +4 -2
  84. package/core/server/services/themes/ThemeStorage.js +5 -5
  85. package/core/server/services/url/Resource.js +1 -1
  86. package/core/server/services/url/Resources.js +28 -21
  87. package/core/server/services/url/UrlService.js +66 -8
  88. package/core/server/services/url/Urls.js +7 -2
  89. package/core/server/services/url/index.js +8 -1
  90. package/core/server/web/admin/views/default-prod.html +4 -4
  91. package/core/server/web/admin/views/default.html +4 -4
  92. package/core/server/web/api/canary/admin/routes.js +28 -4
  93. package/core/server/web/api/middleware/cors.js +7 -7
  94. package/core/server/web/api/middleware/upload.js +117 -10
  95. package/core/server/web/members/app.js +1 -1
  96. package/core/server/web/shared/middlewares/index.js +0 -4
  97. package/core/shared/config/defaults.json +5 -1
  98. package/core/shared/config/helpers.js +4 -0
  99. package/core/shared/config/overrides.json +8 -0
  100. package/core/shared/labs.js +12 -3
  101. package/package.json +47 -46
  102. package/urls.json +597 -0
  103. package/yarn.lock +1073 -1075
  104. package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
  105. package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
  106. package/core/server/services/redirects/settings.js +0 -234
  107. package/core/server/web/shared/middlewares/custom-redirects.js +0 -128
@@ -4,6 +4,9 @@ const {Buffer} = require('buffer');
4
4
  const {randomBytes} = require('crypto');
5
5
  const {URL} = require('url');
6
6
 
7
+ const config = require('../../../shared/config');
8
+ const urlUtils = require('../../../shared/url-utils');
9
+
7
10
  const messages = {
8
11
  incorrectState: 'State did not match.'
9
12
  };
@@ -24,6 +27,7 @@ const redirectURI = 'https://stripe.ghost.org';
24
27
  * @returns {Promise<URL>}
25
28
  */
26
29
  async function getStripeConnectOAuthUrl(setSessionProp, mode = 'live') {
30
+ checkCanConnect();
27
31
  const randomState = randomBytes(16).toString('hex');
28
32
  const state = Buffer.from(JSON.stringify({
29
33
  mode,
@@ -71,6 +75,16 @@ async function getStripeConnectTokenData(encodedData, getSessionProp) {
71
75
  };
72
76
  }
73
77
 
78
+ function checkCanConnect() {
79
+ const siteUrl = urlUtils.getSiteUrl();
80
+ const productionMode = config.get('env') === 'production';
81
+ const siteUrlUsingSSL = /^https/.test(siteUrl);
82
+ const cannotConnectToStripe = productionMode && !siteUrlUsingSSL;
83
+ if (cannotConnectToStripe) {
84
+ throw new errors.BadRequestError('Cannot connect to stripe unless site is using https://');
85
+ }
86
+ }
87
+
74
88
  module.exports = {
75
89
  getStripeConnectOAuthUrl,
76
90
  getStripeConnectTokenData,
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @typedef {import('./oembed').ICustomProvider} ICustomProvider
3
+ * @typedef {import('./oembed').IExternalRequest} IExternalRequest
4
+ */
5
+
6
+ const OPENSEA_PATH_REGEX = /^\/assets\/(0x[a-f0-9]+)\/(\d+)/;
7
+
8
+ /**
9
+ * @implements ICustomProvider
10
+ */
11
+ class NFTOEmbedProvider {
12
+ /**
13
+ * @param {object} dependencies
14
+ */
15
+ constructor(dependencies) {
16
+ this.dependencies = dependencies;
17
+ }
18
+
19
+ /**
20
+ * @param {URL} url
21
+ * @returns {Promise<boolean>}
22
+ */
23
+ async canSupportRequest(url) {
24
+ return url.host === 'opensea.io' && OPENSEA_PATH_REGEX.test(url.pathname);
25
+ }
26
+
27
+ /**
28
+ * @param {URL} url
29
+ * @param {IExternalRequest} externalRequest
30
+ *
31
+ * @returns {Promise<import('oembed-parser').RichTypeData & Object<string, any>>}
32
+ */
33
+ async getOEmbedData(url, externalRequest) {
34
+ const [match, transaction, asset] = url.pathname.match(OPENSEA_PATH_REGEX);
35
+ if (!match) {
36
+ return null;
37
+ }
38
+ const result = await externalRequest(`https://api.opensea.io/api/v1/asset/${transaction}/${asset}/`, {
39
+ json: true
40
+ });
41
+ return {
42
+ version: '1.0',
43
+ type: 'rich',
44
+ title: result.body.name,
45
+ author_name: result.body.creator.user.username,
46
+ author_url: `https://opensea.io/${result.body.creator.user.username}`,
47
+ provider_name: 'OpenSea',
48
+ provider_url: 'https://opensea.io',
49
+ html: `
50
+ <a href="${result.body.permalink}" class="kg-nft-card">
51
+ <img class="kg-nft-image" src="${result.body.image_url}">
52
+ <div class="kg-nft-metadata">
53
+ <div class="kg-nft-header">
54
+ <h4 class="kg-nft-title"> ${result.body.name} </h4>
55
+ </div>
56
+ <div class="kg-nft-creator">
57
+ Created by <span class="kg-nft-creator-name">${result.body.creator.user.username}</span>
58
+ ${(result.body.collection.name ? `&bull; ${result.body.collection.name}` : ``)}
59
+ </div>
60
+ ${(result.body.description ? `<p class="kg-nft-description">${result.body.description}</p>` : ``)}
61
+ </div>
62
+ </a>
63
+ `,
64
+ width: 1000,
65
+ height: 1000,
66
+ noIframe: true
67
+ };
68
+ }
69
+ }
70
+
71
+ module.exports = NFTOEmbedProvider;
@@ -1,4 +1,3 @@
1
- const Promise = require('bluebird');
2
1
  const errors = require('@tryghost/errors');
3
2
  const tpl = require('@tryghost/tpl');
4
3
  const {extract, hasProvider} = require('oembed-parser');
@@ -11,6 +10,10 @@ const messages = {
11
10
  insufficientMetadata: 'URL contains insufficient metadata.'
12
11
  };
13
12
 
13
+ /**
14
+ * @param {string} url
15
+ * @returns {{url: string, provider: boolean}}
16
+ */
14
17
  const findUrlWithProvider = (url) => {
15
18
  let provider;
16
19
 
@@ -44,6 +47,12 @@ const findUrlWithProvider = (url) => {
44
47
  * @typedef {(url: string, config: Object) => Promise} IExternalRequest
45
48
  */
46
49
 
50
+ /**
51
+ * @typedef {object} ICustomProvider
52
+ * @prop {(url: URL) => Promise<boolean>} canSupportRequest
53
+ * @prop {(url: URL, externalRequest: IExternalRequest) => Promise<import('oembed-parser').OembedData>} getOEmbedData
54
+ */
55
+
47
56
  class OEmbed {
48
57
  /**
49
58
  *
@@ -53,29 +62,62 @@ class OEmbed {
53
62
  */
54
63
  constructor({config, externalRequest}) {
55
64
  this.config = config;
56
- this.externalRequest = externalRequest;
65
+ /** @type {IExternalRequest} */
66
+ this.externalRequest = async (url, requestConfig) => {
67
+ if (this.isIpOrLocalhost(url)) {
68
+ return this.unknownProvider(url);
69
+ }
70
+ const response = await externalRequest(url, requestConfig);
71
+ if (this.isIpOrLocalhost(response.url)) {
72
+ return this.unknownProvider(url);
73
+ }
74
+ return response;
75
+ };
76
+ /** @type {ICustomProvider[]} */
77
+ this.customProviders = [];
78
+ }
79
+
80
+ /**
81
+ * @param {ICustomProvider} provider
82
+ */
83
+ registerProvider(provider) {
84
+ this.customProviders.push(provider);
57
85
  }
58
86
 
59
- unknownProvider(url) {
60
- return Promise.reject(new errors.ValidationError({
87
+ /**
88
+ * @param {string} url
89
+ */
90
+ async unknownProvider(url) {
91
+ throw new errors.ValidationError({
61
92
  message: tpl(messages.unknownProvider),
62
93
  context: url
63
- }));
94
+ });
64
95
  }
65
96
 
66
- knownProvider(url) {
67
- return extract(url).catch((err) => {
68
- return Promise.reject(new errors.InternalServerError({
97
+ /**
98
+ * @param {string} url
99
+ */
100
+ async knownProvider(url) {
101
+ try {
102
+ return await extract(url);
103
+ } catch (err) {
104
+ throw new errors.InternalServerError({
69
105
  message: err.message
70
- }));
71
- });
106
+ });
107
+ }
72
108
  }
73
109
 
110
+ /**
111
+ * @param {string} url
112
+ */
74
113
  errorHandler(url) {
75
- return (err) => {
114
+ /**
115
+ * @param {Error|errors.GhostError} err
116
+ */
117
+ return async (err) => {
76
118
  // allow specific validation errors through for better error messages
77
119
  if (errors.utils.isIgnitionError(err) && err.errorType === 'ValidationError') {
78
- return Promise.reject(err);
120
+ throw err;
79
121
  }
80
122
 
81
123
  // default to unknown provider to avoid leaking any app specifics
@@ -97,19 +139,11 @@ class OEmbed {
97
139
 
98
140
  let scraperResponse;
99
141
 
100
- try {
101
- const cookieJar = new CookieJar();
102
- const response = await this.externalRequest(url, {cookieJar});
142
+ const cookieJar = new CookieJar();
143
+ const response = await this.externalRequest(url, {cookieJar});
103
144
 
104
- if (this.isIpOrLocalhost(response.url)) {
105
- scraperResponse = {};
106
- } else {
107
- const html = response.body;
108
- scraperResponse = await metascraper({html, url});
109
- }
110
- } catch (err) {
111
- return Promise.reject(err);
112
- }
145
+ const html = response.body;
146
+ scraperResponse = await metascraper({html, url});
113
147
 
114
148
  const metadata = Object.assign({}, scraperResponse, {
115
149
  thumbnail: scraperResponse.image,
@@ -119,20 +153,25 @@ class OEmbed {
119
153
  delete metadata.image;
120
154
  delete metadata.logo;
121
155
 
122
- if (metadata.title) {
123
- return Promise.resolve({
124
- type: 'bookmark',
125
- url,
126
- metadata
156
+ if (!metadata.title) {
157
+ throw new errors.ValidationError({
158
+ message: tpl(messages.insufficientMetadata),
159
+ context: url
127
160
  });
128
161
  }
129
162
 
130
- return Promise.reject(new errors.ValidationError({
131
- message: tpl(messages.insufficientMetadata),
132
- context: url
133
- }));
163
+ return {
164
+ version: '1.0',
165
+ type: 'bookmark',
166
+ url,
167
+ metadata
168
+ };
134
169
  }
135
170
 
171
+ /**
172
+ * @param {string} url
173
+ * @returns {boolean}
174
+ */
136
175
  isIpOrLocalhost(url) {
137
176
  try {
138
177
  const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
@@ -163,14 +202,7 @@ class OEmbed {
163
202
  *
164
203
  * @returns {Promise<Object>}
165
204
  */
166
- fetchOembedData(_url, cardType) {
167
- // parse the url then validate the protocol and host to make sure it's
168
- // http(s) and not an IP address or localhost to avoid potential access to
169
- // internal network endpoints
170
- if (this.isIpOrLocalhost(_url)) {
171
- return this.unknownProvider();
172
- }
173
-
205
+ async fetchOembedData(_url, cardType) {
174
206
  // check against known oembed list
175
207
  let {url, provider} = findUrlWithProvider(_url);
176
208
  if (provider) {
@@ -180,88 +212,81 @@ class OEmbed {
180
212
  // url not in oembed list so fetch it in case it's a redirect or has a
181
213
  // <link rel="alternate" type="application/json+oembed"> element
182
214
  const cookieJar = new CookieJar();
183
- return this.externalRequest(url, {
215
+ const pageResponse = await this.externalRequest(url, {
184
216
  method: 'GET',
185
217
  timeout: 2 * 1000,
186
218
  followRedirect: true,
187
219
  cookieJar
188
- }).then((pageResponse) => {
189
- // url changed after fetch, see if we were redirected to a known oembed
190
- if (pageResponse.url !== url) {
191
- ({url, provider} = findUrlWithProvider(pageResponse.url));
192
- if (provider) {
193
- return this.knownProvider(url);
194
- }
220
+ });
221
+ // url changed after fetch, see if we were redirected to a known oembed
222
+ if (pageResponse.url !== url) {
223
+ ({url, provider} = findUrlWithProvider(pageResponse.url));
224
+ if (provider) {
225
+ return this.knownProvider(url);
195
226
  }
227
+ }
196
228
 
197
- // check for <link rel="alternate" type="application/json+oembed"> element
198
- let oembedUrl;
199
- try {
200
- oembedUrl = cheerio('link[type="application/json+oembed"]', pageResponse.body).attr('href');
201
- } catch (e) {
202
- return this.unknownProvider(url);
229
+ // check for <link rel="alternate" type="application/json+oembed"> element
230
+ let oembedUrl;
231
+ try {
232
+ oembedUrl = cheerio('link[type="application/json+oembed"]', pageResponse.body).attr('href');
233
+ } catch (e) {
234
+ return this.unknownProvider(url);
235
+ }
236
+
237
+ if (oembedUrl) {
238
+ // for standard WP oembed's we want to insert a bookmark card rather than their blockquote+script
239
+ // which breaks in the editor and most Ghost themes. Only fallback if card type was not explicitly chosen
240
+ if (!cardType && oembedUrl.match(/wp-json\/oembed/)) {
241
+ return;
203
242
  }
204
243
 
205
- if (oembedUrl) {
206
- // make sure the linked url is not an ip address or localhost
207
- if (this.isIpOrLocalhost(oembedUrl)) {
208
- return this.unknownProvider(oembedUrl);
244
+ // fetch oembed response from embedded rel="alternate" url
245
+ const oembedResponse = await this.externalRequest(oembedUrl, {
246
+ method: 'GET',
247
+ json: true,
248
+ timeout: 2 * 1000,
249
+ followRedirect: true,
250
+ cookieJar
251
+ });
252
+ // validate the fetched json against the oembed spec to avoid
253
+ // leaking non-oembed responses
254
+ const body = oembedResponse.body;
255
+ const hasRequiredFields = body.type && body.version;
256
+ const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);
257
+
258
+ if (hasRequiredFields && hasValidType) {
259
+ // extract known oembed fields from the response to limit leaking of unrecognised data
260
+ const knownFields = [
261
+ 'type',
262
+ 'version',
263
+ 'html',
264
+ 'url',
265
+ 'title',
266
+ 'width',
267
+ 'height',
268
+ 'author_name',
269
+ 'author_url',
270
+ 'provider_name',
271
+ 'provider_url',
272
+ 'thumbnail_url',
273
+ 'thumbnail_width',
274
+ 'thumbnail_height'
275
+ ];
276
+ const oembed = _.pick(body, knownFields);
277
+
278
+ // ensure we have required data for certain types
279
+ if (oembed.type === 'photo' && !oembed.url) {
280
+ return;
209
281
  }
210
-
211
- // for standard WP oembed's we want to insert a bookmark card rather than their blockquote+script
212
- // which breaks in the editor and most Ghost themes. Only fallback if card type was not explicitly chosen
213
- if (!cardType && oembedUrl.match(/wp-json\/oembed/)) {
282
+ if ((oembed.type === 'video' || oembed.type === 'rich') && (!oembed.html || !oembed.width || !oembed.height)) {
214
283
  return;
215
284
  }
216
285
 
217
- // fetch oembed response from embedded rel="alternate" url
218
- return this.externalRequest(oembedUrl, {
219
- method: 'GET',
220
- json: true,
221
- timeout: 2 * 1000,
222
- followRedirect: true,
223
- cookieJar
224
- }).then((oembedResponse) => {
225
- // validate the fetched json against the oembed spec to avoid
226
- // leaking non-oembed responses
227
- const body = oembedResponse.body;
228
- const hasRequiredFields = body.type && body.version;
229
- const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);
230
-
231
- if (hasRequiredFields && hasValidType) {
232
- // extract known oembed fields from the response to limit leaking of unrecognised data
233
- const knownFields = [
234
- 'type',
235
- 'version',
236
- 'html',
237
- 'url',
238
- 'title',
239
- 'width',
240
- 'height',
241
- 'author_name',
242
- 'author_url',
243
- 'provider_name',
244
- 'provider_url',
245
- 'thumbnail_url',
246
- 'thumbnail_width',
247
- 'thumbnail_height'
248
- ];
249
- const oembed = _.pick(body, knownFields);
250
-
251
- // ensure we have required data for certain types
252
- if (oembed.type === 'photo' && !oembed.url) {
253
- return;
254
- }
255
- if ((oembed.type === 'video' || oembed.type === 'rich') && (!oembed.html || !oembed.width || !oembed.height)) {
256
- return;
257
- }
258
-
259
- // return the extracted object, don't pass through the response body
260
- return oembed;
261
- }
262
- }).catch(() => {});
286
+ // return the extracted object, don't pass through the response body
287
+ return oembed;
263
288
  }
264
- });
289
+ }
265
290
  }
266
291
 
267
292
  /**
@@ -274,6 +299,16 @@ class OEmbed {
274
299
  let data;
275
300
 
276
301
  try {
302
+ const urlObject = new URL(url);
303
+ for (const provider of this.customProviders) {
304
+ if (await provider.canSupportRequest(urlObject)) {
305
+ const result = await provider.getOEmbedData(urlObject, this.externalRequest);
306
+ if (result !== null) {
307
+ return result;
308
+ }
309
+ }
310
+ }
311
+
277
312
  if (type === 'bookmark') {
278
313
  return this.fetchBookmarkData(url);
279
314
  }
@@ -1,6 +1,3 @@
1
- const labs = require('../../../shared/labs');
2
- const events = require('../../lib/common/events');
3
-
4
1
  const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
5
2
  const OffersModule = require('@tryghost/members-offers');
6
3
 
@@ -28,34 +25,7 @@ module.exports = {
28
25
 
29
26
  this.api = offersModule.api;
30
27
 
31
- let initCalled = false;
32
- if (labs.isSet('offers')) {
33
- // handles setting up redirects
34
- const promise = offersModule.init();
35
- initCalled = true;
36
- await promise;
37
- }
38
-
39
- // TODO: Delete after GA
40
- let offersEnabled = labs.isSet('offers');
41
- events.on('settings.labs.edited', async () => {
42
- if (labs.isSet('offers') && !initCalled) {
43
- const promise = offersModule.init();
44
- initCalled = true;
45
- await promise;
46
- } else if (labs.isSet('offers') !== offersEnabled) {
47
- offersEnabled = labs.isSet('offers');
48
-
49
- if (offersEnabled) {
50
- const offers = await this.api.listOffers({});
51
- for (const offer of offers) {
52
- redirectManager.addRedirect(`/${offer.code}`, `/#/portal/offers/${offer.id}`, {permanent: false});
53
- }
54
- } else {
55
- redirectManager.removeAllRedirects();
56
- }
57
- }
58
- });
28
+ await offersModule.init();
59
29
  },
60
30
 
61
31
  api: null,
@@ -16,7 +16,8 @@ module.exports = function getConfigProperties() {
16
16
  stripeDirect: config.get('stripeDirect'),
17
17
  mailgunIsConfigured: config.get('bulkEmail') && config.get('bulkEmail').mailgun,
18
18
  emailAnalytics: config.get('emailAnalytics'),
19
- hostSettings: config.get('hostSettings')
19
+ hostSettings: config.get('hostSettings'),
20
+ tenorApiKey: config.get('tenorApiKey')
20
21
  };
21
22
 
22
23
  const billingUrl = config.get('hostSettings:billing:enabled') ? config.get('hostSettings:billing:url') : '';