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
@@ -777,11 +777,11 @@
777
777
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
778
778
  font-size: 0.95em;
779
779
  font-weight: 600;
780
- text-decoration: none !important;
780
+ text-decoration: none;
781
781
  border-radius: 5px;
782
782
  transition: opacity 0.2s ease-in-out;
783
783
  background-color: var(--ghost-accent-color);
784
- color: #ffffff !important;
784
+ color: #ffffff;
785
785
  margin: 1.75em 0 0;
786
786
  }
787
787
 
@@ -800,12 +800,12 @@
800
800
  .kg-header-card.kg-style-image a.kg-header-card-button,
801
801
  .kg-header-card.kg-style-dark a.kg-header-card-button {
802
802
  background: #ffffff;
803
- color: #15171a !important;
803
+ color: #15171a;
804
804
  }
805
805
 
806
806
  .kg-header-card.kg-style-accent a.kg-header-card-button {
807
807
  background: #ffffff;
808
- color: var(--ghost-accent-color) !important;
808
+ color: var(--ghost-accent-color);
809
809
  }
810
810
 
811
811
  .kg-audio-card {
@@ -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);
@@ -51,7 +51,7 @@ function getDescription(data, root, options = {}) {
51
51
  || settingsCache.get('description')
52
52
  || '';
53
53
  } else {
54
- description = data.post.meta_description || '';
54
+ description = data.post.meta_description || data.post.custom_excerpt || '';
55
55
  }
56
56
  } else if (_.includes(context, 'page') && data.post) {
57
57
  // Page description dependent on legacy object formatting (https://github.com/TryGhost/Ghost/issues/10042)
@@ -63,7 +63,7 @@ function getDescription(data, root, options = {}) {
63
63
  || settingsCache.get('description')
64
64
  || '';
65
65
  } else {
66
- description = data.post.meta_description || '';
66
+ description = data.post.meta_description || data.post.custom_excerpt || '';
67
67
  }
68
68
  } else if (_.includes(context, 'page') && data.page) {
69
69
  if (options.property) {
@@ -74,7 +74,7 @@ function getDescription(data, root, options = {}) {
74
74
  || settingsCache.get('description')
75
75
  || '';
76
76
  } else {
77
- description = data.page.meta_description || '';
77
+ description = data.page.meta_description || data.page.custom_excerpt || '';
78
78
  }
79
79
  }
80
80
 
@@ -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
  };
@@ -17,12 +17,12 @@ class BaseSiteMapGenerator {
17
17
  constructor() {
18
18
  this.nodeLookup = {};
19
19
  this.nodeTimeLookup = {};
20
- this.siteMapContent = null;
20
+ this.siteMapContent = new Map();
21
21
  this.lastModified = 0;
22
- this.maxNodes = 50000;
22
+ this.maxPerPage = 50000;
23
23
  }
24
24
 
25
- generateXmlFromNodes() {
25
+ generateXmlFromNodes(page) {
26
26
  // Get a mapping of node to timestamp
27
27
  let nodesToProcess = _.map(this.nodeLookup, (node, id) => {
28
28
  return {
@@ -33,20 +33,23 @@ class BaseSiteMapGenerator {
33
33
  };
34
34
  });
35
35
 
36
- // Limit to 50k nodes - this is a quick fix to prevent errors in google console
37
- if (this.maxNodes) {
38
- nodesToProcess = nodesToProcess.slice(0, this.maxNodes);
39
- }
40
-
41
36
  // Sort nodes by timestamp
42
37
  nodesToProcess = _.sortBy(nodesToProcess, 'ts');
43
38
 
39
+ // Get the page of nodes that was requested
40
+ nodesToProcess = nodesToProcess.slice((page - 1) * this.maxPerPage, page * this.maxPerPage);
41
+
42
+ // Do not generate empty sitemaps
43
+ if (nodesToProcess.length === 0) {
44
+ return null;
45
+ }
46
+
44
47
  // Grab just the nodes
45
- nodesToProcess = _.map(nodesToProcess, 'node');
48
+ const nodes = _.map(nodesToProcess, 'node');
46
49
 
47
50
  const data = {
48
51
  // Concat the elements to the _attr declaration
49
- urlset: [XMLNS_DECLS].concat(nodesToProcess)
52
+ urlset: [XMLNS_DECLS].concat(nodes)
50
53
  };
51
54
 
52
55
  // Generate full xml
@@ -67,7 +70,7 @@ class BaseSiteMapGenerator {
67
70
  this.updateLastModified(datum);
68
71
  this.updateLookups(datum, node);
69
72
  // force regeneration of xml
70
- this.siteMapContent = null;
73
+ this.siteMapContent.clear();
71
74
  }
72
75
  }
73
76
 
@@ -75,7 +78,7 @@ class BaseSiteMapGenerator {
75
78
  this.removeFromLookups(datum);
76
79
 
77
80
  // force regeneration of xml
78
- this.siteMapContent = null;
81
+ this.siteMapContent.clear();
79
82
  this.lastModified = Date.now();
80
83
  }
81
84
 
@@ -152,13 +155,13 @@ class BaseSiteMapGenerator {
152
155
  return !!imageUrl;
153
156
  }
154
157
 
155
- getXml() {
156
- if (this.siteMapContent) {
157
- return this.siteMapContent;
158
+ getXml(page = 1) {
159
+ if (this.siteMapContent.has(page)) {
160
+ return this.siteMapContent.get(page);
158
161
  }
159
162
 
160
- const content = this.generateXmlFromNodes();
161
- this.siteMapContent = content;
163
+ const content = this.generateXmlFromNodes(page);
164
+ this.siteMapContent.set(page, content);
162
165
  return content;
163
166
  }
164
167
 
@@ -181,7 +184,7 @@ class BaseSiteMapGenerator {
181
184
  reset() {
182
185
  this.nodeLookup = {};
183
186
  this.nodeTimeLookup = {};
184
- this.siteMapContent = null;
187
+ this.siteMapContent.clear();
185
188
  }
186
189
  }
187
190
 
@@ -5,7 +5,8 @@ const manager = new Manager();
5
5
  // Responsible for handling requests for sitemap files
6
6
  module.exports = function handler(siteApp) {
7
7
  const verifyResourceType = function verifyResourceType(req, res, next) {
8
- if (!Object.prototype.hasOwnProperty.call(manager, req.params.resource)) {
8
+ const resourceWithoutPage = req.params.resource.replace(/-\d+$/, '');
9
+ if (!Object.prototype.hasOwnProperty.call(manager, resourceWithoutPage)) {
9
10
  return res.sendStatus(404);
10
11
  }
11
12
 
@@ -22,14 +23,22 @@ module.exports = function handler(siteApp) {
22
23
  });
23
24
 
24
25
  siteApp.get('/sitemap-:resource.xml', verifyResourceType, function sitemapResourceXML(req, res) {
25
- const type = req.params.resource;
26
- const page = 1;
26
+ const type = req.params.resource.replace(/-\d+$/, '');
27
+ const pageParam = (req.params.resource.match(/-(\d+)$/) || [null, null])[1];
28
+ const page = pageParam ? parseInt(pageParam, 10) : 1;
29
+
30
+ const content = manager.getSiteMapXml(type, page);
31
+ // Prevent x-1.xml as it is a duplicate of x.xml and empty sitemaps
32
+ // (except for the first page so that at least one sitemap exists per type)
33
+ if (pageParam === '1' || (!content && page !== 1)) {
34
+ return res.sendStatus(404);
35
+ }
27
36
 
28
37
  res.set({
29
38
  'Cache-Control': 'public, max-age=' + config.get('caching:sitemap:maxAge'),
30
39
  'Content-Type': 'text/xml'
31
40
  });
32
41
 
33
- res.send(manager.getSiteMapXml(type, page));
42
+ res.send(content);
34
43
  });
35
44
  };
@@ -14,6 +14,7 @@ class SiteMapIndexGenerator {
14
14
  constructor(options) {
15
15
  options = options || {};
16
16
  this.types = options.types;
17
+ this.maxPerPage = options.maxPerPage;
17
18
  }
18
19
 
19
20
  getXml() {
@@ -30,16 +31,25 @@ class SiteMapIndexGenerator {
30
31
 
31
32
  generateSiteMapUrlElements() {
32
33
  return _.map(this.types, (resourceType) => {
33
- const url = urlUtils.urlFor({relativeUrl: '/sitemap-' + resourceType.name + '.xml'}, true);
34
- const lastModified = resourceType.lastModified;
35
-
36
- return {
37
- sitemap: [
38
- {loc: url},
39
- {lastmod: moment(lastModified).toISOString()}
40
- ]
41
- };
42
- });
34
+ // `|| 1` = even if there are no items we still have an empty sitemap file
35
+ const noOfPages = Math.ceil(Object.keys(resourceType.nodeLookup).length / this.maxPerPage) || 1;
36
+ const pages = [];
37
+
38
+ for (let i = 0; i < noOfPages; i++) {
39
+ const page = i === 0 ? '' : `-${i + 1}`;
40
+ const url = urlUtils.urlFor({relativeUrl: '/sitemap-' + resourceType.name + page + '.xml'}, true);
41
+ const lastModified = resourceType.lastModified;
42
+
43
+ pages.push({
44
+ sitemap: [
45
+ {loc: url},
46
+ {lastmod: moment(lastModified).toISOString()}
47
+ ]
48
+ });
49
+ }
50
+
51
+ return pages;
52
+ }).flat();
43
53
  }
44
54
  }
45
55
 
@@ -11,11 +11,13 @@ class SiteMapManager {
11
11
  constructor(options) {
12
12
  options = options || {};
13
13
 
14
+ options.maxPerPage = options.maxPerPage || 50000;
15
+
14
16
  this.pages = options.pages || this.createPagesGenerator(options);
15
17
  this.posts = options.posts || this.createPostsGenerator(options);
16
18
  this.users = this.authors = options.authors || this.createUsersGenerator(options);
17
19
  this.tags = options.tags || this.createTagsGenerator(options);
18
- this.index = options.index || this.createIndexGenerator();
20
+ this.index = options.index || this.createIndexGenerator(options);
19
21
 
20
22
  events.on('router.created', (router) => {
21
23
  if (router.name === 'StaticRoutesRouter') {
@@ -43,14 +45,15 @@ class SiteMapManager {
43
45
  });
44
46
  }
45
47
 
46
- createIndexGenerator() {
48
+ createIndexGenerator(options) {
47
49
  return new IndexMapGenerator({
48
50
  types: {
49
51
  pages: this.pages,
50
52
  posts: this.posts,
51
53
  authors: this.authors,
52
54
  tags: this.tags
53
- }
55
+ },
56
+ maxPerPage: options.maxPerPage
54
57
  });
55
58
  }
56
59
 
@@ -74,8 +77,8 @@ class SiteMapManager {
74
77
  return this.index.getXml();
75
78
  }
76
79
 
77
- getSiteMapXml(type) {
78
- return this[type].getXml();
80
+ getSiteMapXml(type, page) {
81
+ return this[type].getXml(page);
79
82
  }
80
83
  }
81
84
 
@@ -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
 
@@ -2,7 +2,6 @@ const _ = require('lodash');
2
2
  const hbs = require('../engine');
3
3
  const urlUtils = require('../../../../shared/url-utils');
4
4
  const customThemeSettingsCache = require('../../../../shared/custom-theme-settings-cache');
5
- const labs = require('../../../../shared/labs');
6
5
  const preview = require('../preview');
7
6
 
8
7
  function updateLocalTemplateOptions(req, res, next) {
@@ -17,12 +16,8 @@ function updateLocalTemplateOptions(req, res, next) {
17
16
  const previewData = preview.handle(req, Object.keys(customThemeSettingsCache.getAll()));
18
17
 
19
18
  // strip custom off of preview data so it doesn't get merged into @site
20
- const customThemeSettingsPreviewData = previewData.custom;
19
+ const customData = previewData.custom;
21
20
  delete previewData.custom;
22
- let customData = {};
23
- if (labs.isSet('customThemeSettings')) {
24
- customData = customThemeSettingsPreviewData;
25
- }
26
21
 
27
22
  // update site data with any preview values from the request
28
23
  Object.assign(siteData, previewData);
@@ -1,3 +1,8 @@
1
+ .kg-audio-card,
2
+ .kg-audio-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-audio-card {
2
7
  display: flex;
3
8
  width: 100%;
@@ -1,3 +1,8 @@
1
+ .kg-bookmark-card,
2
+ .kg-bookmark-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-bookmark-card,
2
7
  .kg-bookmark-publisher {
3
8
  position: relative;
@@ -1,3 +1,8 @@
1
+ .kg-button-card,
2
+ .kg-button-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-button-card {
2
7
  display: flex;
3
8
  position: static;
@@ -1,3 +1,8 @@
1
+ .kg-callout-card,
2
+ .kg-callout-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-callout-card {
2
7
  display: flex;
3
8
  padding: 1.2em 1.6em;
@@ -1,3 +1,8 @@
1
+ .kg-file-card,
2
+ .kg-file-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-file-card {
2
7
  display: flex;
3
8
  }
@@ -41,7 +46,7 @@
41
46
  }
42
47
 
43
48
  .kg-file-card-title + .kg-file-card-caption {
44
- margin-top: -6px;
49
+ margin-top: -3px;
45
50
  }
46
51
 
47
52
  .kg-file-card-metadata {
@@ -1,3 +1,8 @@
1
+ .kg-gallery-card,
2
+ .kg-gallery-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-gallery-card,
2
7
  .kg-image-card {
3
8
  --gap: 1.2rem;
@@ -1,3 +1,8 @@
1
+ .kg-header-card,
2
+ .kg-header-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-header-card {
2
7
  padding: 12vmin 4em;
3
8
  min-height: 60vh;
@@ -1,3 +1,8 @@
1
+ .kg-nft-card,
2
+ .kg-nft-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-nft-card {
2
7
  display: flex;
3
8
  flex-direction: column;
@@ -1,3 +1,8 @@
1
+ .kg-product-card,
2
+ .kg-product-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-product-card {
2
7
  display: flex;
3
8
  align-items: center;
@@ -1,3 +1,8 @@
1
+ .kg-toggle-card,
2
+ .kg-toggle-card * {
3
+ box-sizing: border-box;
4
+ }
5
+
1
6
  .kg-toggle-card {
2
7
  background: transparent;
3
8
  box-shadow: inset 0 0 0 1px rgba(124, 139, 154, 0.25);
@@ -1,3 +1,7 @@
1
+ .kg-video-card,
2
+ .kg-video-card * {
3
+ box-sizing: border-box;
4
+ }
1
5
 
2
6
  .kg-video-card {
3
7
  position: relative;
@@ -33,16 +33,21 @@
33
33
  </a>
34
34
  </nav>
35
35
  </header>
36
-
36
+
37
37
  <div class="gh-flow-content-wrap">
38
38
  <section class="gh-flow-content gh-flow-content-unsubscribe">
39
- <p>
40
- {{#if error}}
41
- {{error}}
42
- {{else}}
43
- {{#if member}}<span class="gh-flow-em">{{member.email}}</span> has been successfully unsubscribed.{{/if}}
39
+ {{#if error}}
40
+ <p>{{error}}</p>
41
+ {{else}}
42
+ {{#if member}}
43
+ <p>
44
+ <span class="gh-flow-em">{{member.email}}</span> has been successfully unsubscribed from emails.
45
+ <br>
46
+ Don't worry, this will not cancel your paid subscription to {{@site.title}}.
47
+ </p>
48
+ <p>Didn't mean to do this? Manage your account <a href="{{@site.url}}/#/portal/account">here</a>.</p>
44
49
  {{/if}}
45
- </p>
50
+ {{/if}}
46
51
  </section>
47
52
  </div>
48
53
  </div>
@@ -22,7 +22,6 @@ const siteRoutes = require('./routes');
22
22
  const shared = require('../../server/web/shared');
23
23
  const errorHandler = require('@tryghost/mw-error-handler');
24
24
  const mw = require('./middleware');
25
- const labs = require('../../shared/labs');
26
25
 
27
26
  const STATIC_IMAGE_URL_PREFIX = `/${urlUtils.STATIC_IMAGE_URL_PREFIX}`;
28
27
  const STATIC_MEDIA_URL_PREFIX = `/${constants.STATIC_MEDIA_URL_PREFIX}`;
@@ -118,15 +117,19 @@ module.exports = function setupSiteApp(options = {}) {
118
117
  // Serve blog images using the storage adapter
119
118
  siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
120
119
  // Serve blog media using the storage adapter
121
- siteApp.use(STATIC_MEDIA_URL_PREFIX, labs.enabledMiddleware('mediaAPI'), storage.getStorage('media').serve());
120
+ siteApp.use(STATIC_MEDIA_URL_PREFIX, storage.getStorage('media').serve());
122
121
  // Serve blog files using the storage adapter
123
- siteApp.use(STATIC_FILES_URL_PREFIX, labs.enabledMiddleware('filesAPI'), storage.getStorage('files').serve());
122
+ siteApp.use(STATIC_FILES_URL_PREFIX, storage.getStorage('files').serve());
124
123
 
125
124
  // Global handling for member session, ensures a member is logged in to the frontend
126
125
  siteApp.use(membersService.middleware.loadMemberSession);
127
126
 
128
127
  // /member/.well-known/* serves files (e.g. jwks.json) so it needs to be mounted before the prettyUrl mw to avoid trailing slashes
129
- 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
+ );
130
133
 
131
134
  // setup middleware for internal apps
132
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
+ );