ghost 5.55.1 → 5.56.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 (172) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.56.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.56.0.tgz +0 -0
  3. package/components/{tryghost-adapter-manager-5.55.1.tgz → tryghost-adapter-manager-5.56.0.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.56.0.tgz +0 -0
  5. package/components/{tryghost-api-framework-5.55.1.tgz → tryghost-api-framework-5.56.0.tgz} +0 -0
  6. package/components/tryghost-api-version-compatibility-service-5.56.0.tgz +0 -0
  7. package/components/tryghost-audience-feedback-5.56.0.tgz +0 -0
  8. package/components/tryghost-bootstrap-socket-5.56.0.tgz +0 -0
  9. package/components/tryghost-collections-5.56.0.tgz +0 -0
  10. package/components/tryghost-constants-5.56.0.tgz +0 -0
  11. package/components/{tryghost-custom-theme-settings-service-5.55.1.tgz → tryghost-custom-theme-settings-service-5.56.0.tgz} +0 -0
  12. package/components/{tryghost-data-generator-5.55.1.tgz → tryghost-data-generator-5.56.0.tgz} +0 -0
  13. package/components/tryghost-domain-events-5.56.0.tgz +0 -0
  14. package/components/tryghost-dynamic-routing-events-5.56.0.tgz +0 -0
  15. package/components/tryghost-email-analytics-provider-mailgun-5.56.0.tgz +0 -0
  16. package/components/{tryghost-email-analytics-service-5.55.1.tgz → tryghost-email-analytics-service-5.56.0.tgz} +0 -0
  17. package/components/tryghost-email-content-generator-5.56.0.tgz +0 -0
  18. package/components/tryghost-email-events-5.56.0.tgz +0 -0
  19. package/components/tryghost-email-service-5.56.0.tgz +0 -0
  20. package/components/tryghost-email-suppression-list-5.56.0.tgz +0 -0
  21. package/components/tryghost-event-aware-cache-wrapper-5.56.0.tgz +0 -0
  22. package/components/tryghost-express-dynamic-redirects-5.56.0.tgz +0 -0
  23. package/components/tryghost-external-media-inliner-5.56.0.tgz +0 -0
  24. package/components/tryghost-extract-api-key-5.56.0.tgz +0 -0
  25. package/components/tryghost-html-to-plaintext-5.56.0.tgz +0 -0
  26. package/components/tryghost-i18n-5.56.0.tgz +0 -0
  27. package/components/{tryghost-importer-handler-content-files-5.55.1.tgz → tryghost-importer-handler-content-files-5.56.0.tgz} +0 -0
  28. package/components/tryghost-importer-revue-5.56.0.tgz +0 -0
  29. package/components/tryghost-in-memory-repository-5.56.0.tgz +0 -0
  30. package/components/{tryghost-job-manager-5.55.1.tgz → tryghost-job-manager-5.56.0.tgz} +0 -0
  31. package/components/tryghost-link-redirects-5.56.0.tgz +0 -0
  32. package/components/tryghost-link-replacer-5.56.0.tgz +0 -0
  33. package/components/tryghost-link-tracking-5.56.0.tgz +0 -0
  34. package/components/{tryghost-magic-link-5.55.1.tgz → tryghost-magic-link-5.56.0.tgz} +0 -0
  35. package/components/tryghost-mail-events-5.56.0.tgz +0 -0
  36. package/components/{tryghost-mailgun-client-5.55.1.tgz → tryghost-mailgun-client-5.56.0.tgz} +0 -0
  37. package/components/{tryghost-member-attribution-5.55.1.tgz → tryghost-member-attribution-5.56.0.tgz} +0 -0
  38. package/components/tryghost-member-events-5.56.0.tgz +0 -0
  39. package/components/tryghost-members-api-5.56.0.tgz +0 -0
  40. package/components/tryghost-members-csv-5.56.0.tgz +0 -0
  41. package/components/tryghost-members-events-service-5.56.0.tgz +0 -0
  42. package/components/tryghost-members-importer-5.56.0.tgz +0 -0
  43. package/components/{tryghost-members-offers-5.55.1.tgz → tryghost-members-offers-5.56.0.tgz} +0 -0
  44. package/components/tryghost-members-payments-5.56.0.tgz +0 -0
  45. package/components/tryghost-members-ssr-5.56.0.tgz +0 -0
  46. package/components/{tryghost-members-stripe-service-5.55.1.tgz → tryghost-members-stripe-service-5.56.0.tgz} +0 -0
  47. package/components/{tryghost-mentions-email-report-5.55.1.tgz → tryghost-mentions-email-report-5.56.0.tgz} +0 -0
  48. package/components/tryghost-milestones-5.56.0.tgz +0 -0
  49. package/components/{tryghost-minifier-5.55.1.tgz → tryghost-minifier-5.56.0.tgz} +0 -0
  50. package/components/tryghost-model-to-domain-event-interceptor-5.56.0.tgz +0 -0
  51. package/components/tryghost-mw-api-version-mismatch-5.56.0.tgz +0 -0
  52. package/components/tryghost-mw-cache-control-5.56.0.tgz +0 -0
  53. package/components/{tryghost-mw-error-handler-5.55.1.tgz → tryghost-mw-error-handler-5.56.0.tgz} +0 -0
  54. package/components/tryghost-mw-session-from-token-5.56.0.tgz +0 -0
  55. package/components/tryghost-mw-update-user-last-seen-5.56.0.tgz +0 -0
  56. package/components/tryghost-mw-version-match-5.56.0.tgz +0 -0
  57. package/components/tryghost-mw-vhost-5.56.0.tgz +0 -0
  58. package/components/tryghost-nql-filter-expansions-5.56.0.tgz +0 -0
  59. package/components/tryghost-oembed-service-5.56.0.tgz +0 -0
  60. package/components/{tryghost-package-json-5.55.1.tgz → tryghost-package-json-5.56.0.tgz} +0 -0
  61. package/components/tryghost-post-revisions-5.56.0.tgz +0 -0
  62. package/components/tryghost-posts-service-5.56.0.tgz +0 -0
  63. package/components/tryghost-referrers-5.56.0.tgz +0 -0
  64. package/components/{tryghost-security-5.55.1.tgz → tryghost-security-5.56.0.tgz} +0 -0
  65. package/components/{tryghost-session-service-5.55.1.tgz → tryghost-session-service-5.56.0.tgz} +0 -0
  66. package/components/tryghost-settings-path-manager-5.56.0.tgz +0 -0
  67. package/components/tryghost-slack-notifications-5.56.0.tgz +0 -0
  68. package/components/tryghost-staff-service-5.56.0.tgz +0 -0
  69. package/components/tryghost-stats-service-5.56.0.tgz +0 -0
  70. package/components/{tryghost-tiers-5.55.1.tgz → tryghost-tiers-5.56.0.tgz} +0 -0
  71. package/components/tryghost-update-check-service-5.56.0.tgz +0 -0
  72. package/components/tryghost-verification-trigger-5.56.0.tgz +0 -0
  73. package/components/tryghost-version-notifications-data-service-5.56.0.tgz +0 -0
  74. package/components/{tryghost-webmentions-5.55.1.tgz → tryghost-webmentions-5.56.0.tgz} +0 -0
  75. package/content/themes/casper/assets/built/screen.css +1 -1
  76. package/content/themes/casper/assets/built/screen.css.map +1 -1
  77. package/content/themes/casper/assets/css/screen.css +22 -8
  78. package/content/themes/casper/page.hbs +26 -24
  79. package/core/built/admin/assets/chunk.143.a1c19bf68817d7c7ddf6.js +39 -0
  80. package/core/built/admin/assets/{chunk.178.318362f453e3e1f6655d.js → chunk.178.1e3a659730595eff43b8.js} +4 -4
  81. package/core/built/admin/assets/{chunk.757.c768dcbeaf1cee916395.js → chunk.757.ada6ed049fa8078cd416.js} +16 -17
  82. package/core/built/admin/assets/ghost-864e00075c219a4cb7a202a90a523bc4.css +1 -0
  83. package/core/built/admin/assets/{ghost-135607a3c421c472feb9f59f14c625fb.js → ghost-9081d01d86c90563530801b2304e1f1a.js} +212 -200
  84. package/core/built/admin/assets/ghost-dark-dc299e0aa5731557a3d5ef3583177487.css +1 -0
  85. package/core/built/admin/assets/{vendor-0a8fc851393c473cd269f04fc4e97974.js → vendor-16709e2a05d1300febb44b2e8f759976.js} +4 -4
  86. package/core/built/admin/index.html +6 -6
  87. package/core/frontend/src/cards/css/header_v2.css +58 -81
  88. package/core/frontend/src/cards/css/signup.css +1 -1
  89. package/core/server/api/endpoints/collections-public.js +53 -0
  90. package/core/server/api/endpoints/collections.js +27 -3
  91. package/core/server/api/endpoints/index.js +4 -0
  92. package/core/server/api/endpoints/newsletters.js +1 -1
  93. package/core/server/api/endpoints/utils/serializers/output/mappers/collections.js +8 -16
  94. package/core/server/data/migrations/versions/5.56/2023-07-14-10-11-12-add-email-disabled-field-to-members.js +7 -0
  95. package/core/server/data/migrations/versions/5.56/2023-07-15-10-11-12-update-members-email-disabled-field.js +22 -0
  96. package/core/server/data/schema/schema.js +1 -0
  97. package/core/server/models/member.js +5 -1
  98. package/core/server/models/newsletter.js +10 -0
  99. package/core/server/models/post.js +2 -23
  100. package/core/server/services/collections/PostsRepository.js +3 -1
  101. package/core/server/services/collections/service.js +3 -3
  102. package/core/server/services/members/middleware.js +2 -3
  103. package/core/server/services/segment/DomainEventsAnalytics.js +1 -1
  104. package/core/server/services/url/UrlGenerator.js +2 -21
  105. package/core/server/web/api/endpoints/admin/routes.js +3 -1
  106. package/core/server/web/api/endpoints/content/routes.js +3 -0
  107. package/core/server/web/shared/middleware/api/spam-prevention.js +31 -1
  108. package/core/server/web/shared/middleware/brute.js +13 -0
  109. package/core/shared/config/defaults.json +6 -0
  110. package/core/shared/config/env/config.testing-browser.json +6 -0
  111. package/core/shared/config/env/config.testing-mysql.json +6 -1
  112. package/core/shared/config/env/config.testing.json +6 -0
  113. package/core/shared/labs.js +3 -1
  114. package/package.json +146 -145
  115. package/yarn.lock +895 -3668
  116. package/components/tryghost-adapter-cache-memory-ttl-5.55.1.tgz +0 -0
  117. package/components/tryghost-adapter-cache-redis-5.55.1.tgz +0 -0
  118. package/components/tryghost-announcement-bar-settings-5.55.1.tgz +0 -0
  119. package/components/tryghost-api-version-compatibility-service-5.55.1.tgz +0 -0
  120. package/components/tryghost-audience-feedback-5.55.1.tgz +0 -0
  121. package/components/tryghost-bootstrap-socket-5.55.1.tgz +0 -0
  122. package/components/tryghost-collections-5.55.1.tgz +0 -0
  123. package/components/tryghost-constants-5.55.1.tgz +0 -0
  124. package/components/tryghost-domain-events-5.55.1.tgz +0 -0
  125. package/components/tryghost-dynamic-routing-events-5.55.1.tgz +0 -0
  126. package/components/tryghost-email-analytics-provider-mailgun-5.55.1.tgz +0 -0
  127. package/components/tryghost-email-content-generator-5.55.1.tgz +0 -0
  128. package/components/tryghost-email-events-5.55.1.tgz +0 -0
  129. package/components/tryghost-email-service-5.55.1.tgz +0 -0
  130. package/components/tryghost-email-suppression-list-5.55.1.tgz +0 -0
  131. package/components/tryghost-event-aware-cache-wrapper-5.55.1.tgz +0 -0
  132. package/components/tryghost-express-dynamic-redirects-5.55.1.tgz +0 -0
  133. package/components/tryghost-external-media-inliner-5.55.1.tgz +0 -0
  134. package/components/tryghost-extract-api-key-5.55.1.tgz +0 -0
  135. package/components/tryghost-html-to-plaintext-5.55.1.tgz +0 -0
  136. package/components/tryghost-i18n-5.55.1.tgz +0 -0
  137. package/components/tryghost-importer-revue-5.55.1.tgz +0 -0
  138. package/components/tryghost-in-memory-repository-5.55.1.tgz +0 -0
  139. package/components/tryghost-link-redirects-5.55.1.tgz +0 -0
  140. package/components/tryghost-link-replacer-5.55.1.tgz +0 -0
  141. package/components/tryghost-link-tracking-5.55.1.tgz +0 -0
  142. package/components/tryghost-mail-events-5.55.1.tgz +0 -0
  143. package/components/tryghost-member-events-5.55.1.tgz +0 -0
  144. package/components/tryghost-members-api-5.55.1.tgz +0 -0
  145. package/components/tryghost-members-csv-5.55.1.tgz +0 -0
  146. package/components/tryghost-members-events-service-5.55.1.tgz +0 -0
  147. package/components/tryghost-members-importer-5.55.1.tgz +0 -0
  148. package/components/tryghost-members-payments-5.55.1.tgz +0 -0
  149. package/components/tryghost-members-ssr-5.55.1.tgz +0 -0
  150. package/components/tryghost-milestones-5.55.1.tgz +0 -0
  151. package/components/tryghost-model-to-domain-event-interceptor-5.55.1.tgz +0 -0
  152. package/components/tryghost-mw-api-version-mismatch-5.55.1.tgz +0 -0
  153. package/components/tryghost-mw-cache-control-5.55.1.tgz +0 -0
  154. package/components/tryghost-mw-session-from-token-5.55.1.tgz +0 -0
  155. package/components/tryghost-mw-update-user-last-seen-5.55.1.tgz +0 -0
  156. package/components/tryghost-mw-version-match-5.55.1.tgz +0 -0
  157. package/components/tryghost-mw-vhost-5.55.1.tgz +0 -0
  158. package/components/tryghost-oembed-service-5.55.1.tgz +0 -0
  159. package/components/tryghost-post-revisions-5.55.1.tgz +0 -0
  160. package/components/tryghost-posts-service-5.55.1.tgz +0 -0
  161. package/components/tryghost-referrers-5.55.1.tgz +0 -0
  162. package/components/tryghost-settings-path-manager-5.55.1.tgz +0 -0
  163. package/components/tryghost-slack-notifications-5.55.1.tgz +0 -0
  164. package/components/tryghost-staff-service-5.55.1.tgz +0 -0
  165. package/components/tryghost-stats-service-5.55.1.tgz +0 -0
  166. package/components/tryghost-update-check-service-5.55.1.tgz +0 -0
  167. package/components/tryghost-verification-trigger-5.55.1.tgz +0 -0
  168. package/components/tryghost-version-notifications-data-service-5.55.1.tgz +0 -0
  169. package/core/built/admin/assets/chunk.143.e7f30f0ca62dfd07aa66.js +0 -34
  170. package/core/built/admin/assets/ghost-4c37b19a127806313f64fcd759eaa0e2.css +0 -1
  171. package/core/built/admin/assets/ghost-dark-b80a5d657c788174451fd218428af744.css +0 -1
  172. /package/core/built/admin/assets/{chunk.757.c768dcbeaf1cee916395.js.LICENSE.txt → chunk.757.ada6ed049fa8078cd416.js.LICENSE.txt} +0 -0
@@ -0,0 +1,22 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(knex) {
6
+ logging.info('Setting email_disabled to true for all members that have their email on the suppression list');
7
+
8
+ await knex('members')
9
+ .join('suppressions', 'members.email', 'suppressions.email')
10
+ .update({
11
+ email_disabled: true
12
+ });
13
+ },
14
+ async function down(knex) {
15
+ logging.info('Setting email_disabled to false for all members');
16
+
17
+ await knex('members')
18
+ .update({
19
+ email_disabled: false
20
+ });
21
+ }
22
+ );
@@ -429,6 +429,7 @@ module.exports = {
429
429
  email_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
430
430
  email_opened_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
431
431
  email_open_rate: {type: 'integer', unsigned: true, nullable: true, index: true},
432
+ email_disabled: {type: 'boolean', nullable: false, defaultTo: false},
432
433
  last_seen_at: {type: 'dateTime', nullable: true},
433
434
  last_commented_at: {type: 'dateTime', nullable: true},
434
435
  created_at: {type: 'dateTime', nullable: false},
@@ -471,7 +471,11 @@ const Member = ghostBookshelf.Model.extend({
471
471
  // we use raw queries instead of model relationships because model hydration is expensive
472
472
  const query = ghostBookshelf.knex('members_newsletters')
473
473
  .join('newsletters', 'members_newsletters.newsletter_id', '=', 'newsletters.id')
474
- .where('newsletters.status', 'active')
474
+ .join('members', 'members_newsletters.member_id', '=', 'members.id')
475
+ .where({
476
+ 'newsletters.status': 'active',
477
+ 'members.email_disabled': false
478
+ })
475
479
  .distinct('member_id as id');
476
480
 
477
481
  if (unfilteredOptions.transacting) {
@@ -151,6 +151,16 @@ const Newsletter = ghostBookshelf.Model.extend({
151
151
  .whereRaw('members_newsletters.newsletter_id = newsletters.id')
152
152
  .as('count__members');
153
153
  });
154
+ },
155
+ active_members(modelOrCollection) {
156
+ modelOrCollection.query('columns', 'newsletters.*', (qb) => {
157
+ qb.count('members_newsletters.id')
158
+ .from('members_newsletters')
159
+ .join('members', 'members.id', 'members_newsletters.member_id')
160
+ .whereRaw('members_newsletters.newsletter_id = newsletters.id')
161
+ .andWhere('members.email_disabled', false)
162
+ .as('count__active_members');
163
+ });
154
164
  }
155
165
  };
156
166
  },
@@ -7,6 +7,7 @@ const tpl = require('@tryghost/tpl');
7
7
  const errors = require('@tryghost/errors');
8
8
  const nql = require('@tryghost/nql');
9
9
  const htmlToPlaintext = require('@tryghost/html-to-plaintext');
10
+ const {posts: postExpansions} = require('@tryghost/nql-filter-expansions');
10
11
  const ghostBookshelf = require('./base');
11
12
  const config = require('../../shared/config');
12
13
  const settingsCache = require('../../shared/settings-cache');
@@ -290,28 +291,6 @@ Post = ghostBookshelf.Model.extend({
290
291
  filterExpansions: function filterExpansions() {
291
292
  const postsMetaKeys = _.without(ghostBookshelf.model('PostsMeta').prototype.orderAttributes(), 'posts_meta.id', 'posts_meta.post_id');
292
293
 
293
- const expansions = [{
294
- key: 'primary_tag',
295
- replacement: 'tags.slug',
296
- expansion: 'posts_tags.sort_order:0+tags.visibility:public'
297
- }, {
298
- key: 'primary_author',
299
- replacement: 'authors.slug',
300
- expansion: 'posts_authors.sort_order:0+authors.visibility:public'
301
- }, {
302
- key: 'authors',
303
- replacement: 'authors.slug'
304
- }, {
305
- key: 'author',
306
- replacement: 'authors.slug'
307
- }, {
308
- key: 'tag',
309
- replacement: 'tags.slug'
310
- }, {
311
- key: 'tags',
312
- replacement: 'tags.slug'
313
- }];
314
-
315
294
  const postMetaKeyExpansions = postsMetaKeys.map((pmk) => {
316
295
  return {
317
296
  key: pmk.split('.')[1],
@@ -319,7 +298,7 @@ Post = ghostBookshelf.Model.extend({
319
298
  };
320
299
  });
321
300
 
322
- return expansions.concat(postMetaKeyExpansions);
301
+ return postExpansions.concat(postMetaKeyExpansions);
323
302
  },
324
303
 
325
304
  filterRelations: function filterRelations() {
@@ -20,7 +20,9 @@ class PostsRepository {
20
20
  id: postJson.id,
21
21
  featured: postJson.featured,
22
22
  published_at: this.moment(postJson.published_at).toISOString(true),
23
- tags: postJson.tags.map(tag => tag.slug)
23
+ tags: postJson.tags.map(tag => ({
24
+ slug: tag.slug
25
+ }))
24
26
  };
25
27
  });
26
28
  }
@@ -2,8 +2,8 @@ const {
2
2
  CollectionsService
3
3
  } = require('@tryghost/collections');
4
4
  const BookshelfCollectionsRepository = require('./BookshelfCollectionsRepository');
5
- const labs = require('../../../shared/labs');
6
5
 
6
+ let inited = false;
7
7
  class CollectionsServiceWrapper {
8
8
  /** @type {CollectionsService} */
9
9
  api;
@@ -31,10 +31,10 @@ class CollectionsServiceWrapper {
31
31
  }
32
32
 
33
33
  async init() {
34
- if (!labs.isSet('collections')) {
34
+ if (inited) {
35
35
  return;
36
36
  }
37
-
37
+ inited = true;
38
38
  this.api.subscribeToEvents();
39
39
  require('./intercept-events')();
40
40
  }
@@ -105,11 +105,10 @@ const deleteSuppression = async function (req, res) {
105
105
  try {
106
106
  const member = await membersService.ssr.getMemberDataFromSession(req, res);
107
107
  const options = {
108
- id: member.id,
109
- withRelated: ['newsletters']
108
+ id: member.id
110
109
  };
111
110
  await emailSuppressionList.removeEmail(member.email);
112
- await membersService.api.members.update({subscribed: true}, options);
111
+ await membersService.api.members.update({email_disabled: false}, options);
113
112
 
114
113
  res.writeHead(204);
115
114
  res.end();
@@ -65,7 +65,7 @@ module.exports = class DomainEventsAnalytics {
65
65
  if (event.data.milestone
66
66
  && event.data.milestone.value === 100
67
67
  ) {
68
- const eventName = event.data.milestone.type === 'arr' ? '$100 MRR reached' : '100 Members reached';
68
+ const eventName = event.data.milestone.type === 'arr' ? '$100 ARR reached' : '100 Members reached';
69
69
 
70
70
  try {
71
71
  this.#analytics.track(Object.assign(this.#trackDefaults, {}, {event: this.#prefix + eventName}));
@@ -3,26 +3,7 @@ const nql = require('@tryghost/nql');
3
3
  const debug = require('@tryghost/debug')('services:url:generator');
4
4
  const localUtils = require('../../../shared/url-utils');
5
5
 
6
- // @TODO: merge with filter plugin
7
- const EXPANSIONS = [{
8
- key: 'author',
9
- replacement: 'authors.slug'
10
- }, {
11
- key: 'tags',
12
- replacement: 'tags.slug'
13
- }, {
14
- key: 'tag',
15
- replacement: 'tags.slug'
16
- }, {
17
- key: 'authors',
18
- replacement: 'authors.slug'
19
- }, {
20
- key: 'primary_tag',
21
- replacement: 'primary_tag.slug'
22
- }, {
23
- key: 'primary_author',
24
- replacement: 'primary_author.slug'
25
- }];
6
+ const {posts: postExpansions} = require('@tryghost/nql-filter-expansions');
26
7
 
27
8
  /**
28
9
  * The UrlGenerator class is responsible to generate urls based on a router's conditions.
@@ -57,7 +38,7 @@ class UrlGenerator {
57
38
  if (filter) {
58
39
  this.filter = filter;
59
40
  this.nql = nql(this.filter, {
60
- expansions: EXPANSIONS,
41
+ expansions: postExpansions,
61
42
  transformer: nql.utils.mapKeyValues({
62
43
  key: {
63
44
  from: 'page',
@@ -22,6 +22,7 @@ module.exports = function apiRoutes() {
22
22
  // ## Collections
23
23
  router.get('/collections', mw.authAdminApi, labs.enabledMiddleware('collections'), http(api.collections.browse));
24
24
  router.get('/collections/:id', mw.authAdminApi, labs.enabledMiddleware('collections'), http(api.collections.read));
25
+ router.get('/collections/slug/:slug', mw.authAdminApi, labs.enabledMiddleware('collections'), http(api.collections.read));
25
26
  router.post('/collections', mw.authAdminApi, labs.enabledMiddleware('collections'), http(api.collections.add));
26
27
  router.put('/collections/:id', mw.authAdminApi, labs.enabledMiddleware('collections'), http(api.collections.edit));
27
28
  router.del('/collections/:id', mw.authAdminApi, labs.enabledMiddleware('collections'), http(api.collections.destroy));
@@ -313,7 +314,8 @@ module.exports = function apiRoutes() {
313
314
 
314
315
  // ## Email Preview
315
316
  router.get('/email_previews/posts/:id', mw.authAdminApi, http(api.email_previews.read));
316
- router.post('/email_previews/posts/:id', mw.authAdminApi, http(api.email_previews.sendTestEmail));
317
+ // preview sending have an additional rate limiter to prevent abuse
318
+ router.post('/email_previews/posts/:id', shared.middleware.brute.previewEmailLimiter, mw.authAdminApi, http(api.email_previews.sendTestEmail));
317
319
 
318
320
  // ## Emails
319
321
  router.get('/emails', mw.authAdminApi, http(api.emails.browse));
@@ -39,5 +39,8 @@ module.exports = function apiRoutes() {
39
39
  router.get('/tiers', mw.authenticatePublic, http(api.tiersPublic.browse));
40
40
  router.get('/offers/:id', mw.authenticatePublic, http(api.offersPublic.read));
41
41
 
42
+ router.get('/collections/:id', mw.authenticatePublic, http(api.collectionsPublic.readById));
43
+ router.get('/collections/slug/:slug', mw.authenticatePublic, http(api.collectionsPublic.readBySlug));
44
+
42
45
  return router;
43
46
  };
@@ -21,7 +21,8 @@ const messages = {
21
21
  context: 'Too many login attempts.'
22
22
  },
23
23
  tooManyAttempts: 'Too many attempts.',
24
- webmentionsBlock: 'Too many mention attempts'
24
+ webmentionsBlock: 'Too many mention attempts',
25
+ emailPreviewBlock: 'Only 10 test emails can be sent per hour'
25
26
  };
26
27
  let spamPrivateBlock = spam.private_block || {};
27
28
  let spamGlobalBlock = spam.global_block || {};
@@ -31,6 +32,7 @@ let spamUserLogin = spam.user_login || {};
31
32
  let spamMemberLogin = spam.member_login || {};
32
33
  let spamContentApiKey = spam.content_api_key || {};
33
34
  let spamWebmentionsBlock = spam.webmentions_block || {};
35
+ let spamEmailPreviewBlock = spam.email_preview_block || {};
34
36
 
35
37
  let store;
36
38
  let memoryStore;
@@ -43,6 +45,7 @@ let membersAuthInstance;
43
45
  let membersAuthEnumerationInstance;
44
46
  let userResetInstance;
45
47
  let contentApiKeyInstance;
48
+ let emailPreviewBlockInstance;
46
49
 
47
50
  const spamConfigKeys = ['freeRetries', 'minWait', 'maxWait', 'lifetime'];
48
51
 
@@ -152,6 +155,32 @@ const webmentionsBlock = () => {
152
155
  return webmentionsBlockInstance;
153
156
  };
154
157
 
158
+ const emailPreviewBlock = () => {
159
+ const ExpressBrute = require('express-brute');
160
+ const BruteKnex = require('brute-knex');
161
+ const db = require('../../../../data/db');
162
+
163
+ store = store || new BruteKnex({
164
+ tablename: 'brute',
165
+ createTable: false,
166
+ knex: db.knex
167
+ });
168
+
169
+ emailPreviewBlockInstance = emailPreviewBlockInstance || new ExpressBrute(store,
170
+ extend({
171
+ attachResetToRequest: false,
172
+ failCallback(req, res, next) {
173
+ return next(new errors.TooManyRequestsError({
174
+ message: messages.emailPreviewBlock
175
+ }));
176
+ },
177
+ handleStoreError: handleStoreError
178
+ }, pick(spamEmailPreviewBlock, spamConfigKeys))
179
+ );
180
+
181
+ return emailPreviewBlockInstance;
182
+ };
183
+
155
184
  const membersAuth = () => {
156
185
  const ExpressBrute = require('express-brute');
157
186
  const BruteKnex = require('brute-knex');
@@ -349,6 +378,7 @@ module.exports = {
349
378
  privateBlog: privateBlog,
350
379
  contentApiKey: contentApiKey,
351
380
  webmentionsBlock: webmentionsBlock,
381
+ emailPreviewBlock: emailPreviewBlock,
352
382
  reset: () => {
353
383
  store = undefined;
354
384
  memoryStore = undefined;
@@ -117,5 +117,18 @@ module.exports = {
117
117
  return _next('webmention_blocked');
118
118
  }
119
119
  })(req, res, next);
120
+ },
121
+
122
+ /**
123
+ * Blocks preview email spam
124
+ */
125
+
126
+ previewEmailLimiter(req, res, next) {
127
+ return spamPrevention.emailPreviewBlock().getMiddleware({
128
+ ignoreIP: false,
129
+ key(_req, _res, _next) {
130
+ return _next('preview_email_blocked');
131
+ }
132
+ })(req, res, next);
120
133
  }
121
134
  };
@@ -108,6 +108,12 @@
108
108
  "maxWait": 100,
109
109
  "lifetime": 1000,
110
110
  "freeRetries": 100
111
+ },
112
+ "email_preview_block": {
113
+ "minWait": 360000,
114
+ "maxWait": 360000,
115
+ "lifetime": 3600,
116
+ "freeRetries": 10
111
117
  }
112
118
  },
113
119
  "caching": {
@@ -49,6 +49,12 @@
49
49
  "maxWait": 100000,
50
50
  "lifetime": 3600,
51
51
  "freeRetries": 3
52
+ },
53
+ "email_preview_block": {
54
+ "minWait": 360000,
55
+ "maxWait": 360000,
56
+ "lifetime": 3600,
57
+ "freeRetries": 10
52
58
  }
53
59
  },
54
60
  "privacy": {
@@ -50,8 +50,13 @@
50
50
  "maxWait": 100000,
51
51
  "lifetime": 3600,
52
52
  "freeRetries": 3
53
+ },
54
+ "email_preview_block": {
55
+ "minWait": 360000,
56
+ "maxWait": 360000,
57
+ "lifetime": 3600,
58
+ "freeRetries": 10
53
59
  }
54
-
55
60
  },
56
61
  "privacy": {
57
62
  "useTinfoil": true,
@@ -49,6 +49,12 @@
49
49
  "maxWait": 100000,
50
50
  "lifetime": 3600,
51
51
  "freeRetries": 3
52
+ },
53
+ "email_preview_block": {
54
+ "minWait": 360000,
55
+ "maxWait": 360000,
56
+ "lifetime": 3600,
57
+ "freeRetries": 10
52
58
  }
53
59
  },
54
60
  "privacy": {
@@ -42,7 +42,9 @@ const ALPHA_FEATURES = [
42
42
  'flatUrls',
43
43
  'mailEvents',
44
44
  'collectionsCard',
45
- 'headerUpgrade'
45
+ 'headerUpgrade',
46
+ 'importMemberTier',
47
+ 'tipsAndDonations'
46
48
  ];
47
49
 
48
50
  module.exports.GA_KEYS = [...GA_FEATURES];