ghost 5.10.1 → 5.12.1

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 (150) hide show
  1. package/components/{tryghost-adapter-manager-5.10.1.tgz → tryghost-adapter-manager-5.12.1.tgz} +0 -0
  2. package/components/tryghost-api-framework-5.12.1.tgz +0 -0
  3. package/components/tryghost-api-version-compatibility-service-5.12.1.tgz +0 -0
  4. package/components/{tryghost-bootstrap-socket-5.10.1.tgz → tryghost-bootstrap-socket-5.12.1.tgz} +0 -0
  5. package/components/tryghost-constants-5.12.1.tgz +0 -0
  6. package/components/{tryghost-custom-theme-settings-service-5.10.1.tgz → tryghost-custom-theme-settings-service-5.12.1.tgz} +0 -0
  7. package/components/tryghost-domain-events-5.12.1.tgz +0 -0
  8. package/components/{tryghost-email-analytics-provider-mailgun-5.10.1.tgz → tryghost-email-analytics-provider-mailgun-5.12.1.tgz} +0 -0
  9. package/components/{tryghost-email-analytics-service-5.10.1.tgz → tryghost-email-analytics-service-5.12.1.tgz} +0 -0
  10. package/components/{tryghost-email-content-generator-5.10.1.tgz → tryghost-email-content-generator-5.12.1.tgz} +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.12.1.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.12.1.tgz +0 -0
  13. package/components/{tryghost-html-to-plaintext-5.10.1.tgz → tryghost-html-to-plaintext-5.12.1.tgz} +0 -0
  14. package/components/{tryghost-job-manager-5.10.1.tgz → tryghost-job-manager-5.12.1.tgz} +0 -0
  15. package/components/tryghost-magic-link-5.12.1.tgz +0 -0
  16. package/components/tryghost-mailgun-client-5.12.1.tgz +0 -0
  17. package/components/tryghost-member-analytics-service-5.12.1.tgz +0 -0
  18. package/components/tryghost-member-attribution-5.12.1.tgz +0 -0
  19. package/components/tryghost-member-events-5.12.1.tgz +0 -0
  20. package/components/{tryghost-members-analytics-ingress-5.10.1.tgz → tryghost-members-analytics-ingress-5.12.1.tgz} +0 -0
  21. package/components/tryghost-members-api-5.12.1.tgz +0 -0
  22. package/components/{tryghost-members-csv-5.10.1.tgz → tryghost-members-csv-5.12.1.tgz} +0 -0
  23. package/components/{tryghost-members-events-service-5.10.1.tgz → tryghost-members-events-service-5.12.1.tgz} +0 -0
  24. package/components/tryghost-members-importer-5.12.1.tgz +0 -0
  25. package/components/tryghost-members-offers-5.12.1.tgz +0 -0
  26. package/components/{tryghost-members-payments-5.10.1.tgz → tryghost-members-payments-5.12.1.tgz} +0 -0
  27. package/components/tryghost-members-ssr-5.12.1.tgz +0 -0
  28. package/components/{tryghost-members-stripe-service-5.10.1.tgz → tryghost-members-stripe-service-5.12.1.tgz} +0 -0
  29. package/components/tryghost-minifier-5.12.1.tgz +0 -0
  30. package/components/tryghost-mw-api-version-mismatch-5.12.1.tgz +0 -0
  31. package/components/{tryghost-mw-cache-control-5.10.1.tgz → tryghost-mw-cache-control-5.12.1.tgz} +0 -0
  32. package/components/tryghost-mw-error-handler-5.12.1.tgz +0 -0
  33. package/components/{tryghost-mw-session-from-token-5.10.1.tgz → tryghost-mw-session-from-token-5.12.1.tgz} +0 -0
  34. package/components/tryghost-mw-update-user-last-seen-5.12.1.tgz +0 -0
  35. package/components/tryghost-mw-vhost-5.12.1.tgz +0 -0
  36. package/components/{tryghost-oembed-service-5.10.1.tgz → tryghost-oembed-service-5.12.1.tgz} +0 -0
  37. package/components/{tryghost-package-json-5.10.1.tgz → tryghost-package-json-5.12.1.tgz} +0 -0
  38. package/components/{tryghost-security-5.10.1.tgz → tryghost-security-5.12.1.tgz} +0 -0
  39. package/components/{tryghost-session-service-5.10.1.tgz → tryghost-session-service-5.12.1.tgz} +0 -0
  40. package/components/tryghost-settings-path-manager-5.12.1.tgz +0 -0
  41. package/components/tryghost-staff-service-5.12.1.tgz +0 -0
  42. package/components/{tryghost-update-check-service-5.10.1.tgz → tryghost-update-check-service-5.12.1.tgz} +0 -0
  43. package/components/tryghost-verification-trigger-5.12.1.tgz +0 -0
  44. package/components/{tryghost-version-notifications-data-service-5.10.1.tgz → tryghost-version-notifications-data-service-5.12.1.tgz} +0 -0
  45. package/core/boot.js +2 -0
  46. package/core/built/admin/assets/chunk.143.cae75c9fa9b7050ffee8.js +49 -0
  47. package/core/built/admin/assets/{chunk.174.0364e8abdae8210d8e6d.js → chunk.174.ae492405065373dbe102.js} +1 -1
  48. package/core/built/admin/assets/{chunk.178.bb46965ba0483c3e79ae.js → chunk.178.0487ee9895eefb6e2baf.js} +4 -4
  49. package/core/built/admin/assets/{chunk.351.ea4a4ff4b40d5f2ad141.js → chunk.579.65e09dd89eec70d059a0.js} +3 -11
  50. package/core/built/admin/assets/{chunk.351.ea4a4ff4b40d5f2ad141.js.LICENSE.txt → chunk.579.65e09dd89eec70d059a0.js.LICENSE.txt} +0 -0
  51. package/core/built/admin/assets/{ghost-ced03a7ac75c3148e0ea7d1bf51e39fc.js → ghost-0526c96b20843697927c1d06a9010197.js} +779 -610
  52. package/core/built/admin/assets/ghost-0bbbef127e5dc0c0651fc442c4fdba8e.css +1 -0
  53. package/core/built/admin/assets/ghost-dark-27e002e66fbfdfaf3efcde63d5429c38.css +1 -0
  54. package/core/built/admin/assets/icons/calendar-stroke.svg +1 -0
  55. package/core/built/admin/assets/icons/event-canceled-subscription--feature-attribution.svg +6 -0
  56. package/core/built/admin/assets/icons/event-comment--feature-attribution.svg +3 -0
  57. package/core/built/admin/assets/icons/event-email-delivery-failed--feature-attribution.svg +6 -0
  58. package/core/built/admin/assets/icons/event-logged-in--feature-attribution.svg +5 -0
  59. package/core/built/admin/assets/icons/event-made-a-payment--feature-attribution.svg +7 -0
  60. package/core/built/admin/assets/icons/event-opened-email--feature-attribution.svg +6 -0
  61. package/core/built/admin/assets/icons/event-received-email--feature-attribution.svg +5 -0
  62. package/core/built/admin/assets/icons/event-signed-up--feature-attribution.svg +6 -0
  63. package/core/built/admin/assets/icons/event-started-subscription--feature-attribution.svg +6 -0
  64. package/core/built/admin/assets/icons/event-subscribed-to-email--feature-attribution.svg +8 -0
  65. package/core/built/admin/assets/icons/event-subscriptions--feature-attribution.svg +5 -0
  66. package/core/built/admin/assets/icons/event-unsubscribed-from-email--feature-attribution.svg +5 -0
  67. package/core/built/admin/assets/icons/pen-stroke.svg +1 -0
  68. package/core/built/admin/assets/{vendor-a1ae7a38d5c38fcba5609eed4e37f02a.js → vendor-52613f40d62355e9ac64cbfa211169bb.js} +88 -60
  69. package/core/built/admin/index.html +6 -6
  70. package/core/frontend/helpers/search.js +5 -20
  71. package/core/frontend/meta/get-meta.js +1 -2
  72. package/core/frontend/meta/image-dimensions.js +47 -39
  73. package/core/server/api/endpoints/comments-members.js +10 -7
  74. package/core/server/api/endpoints/invites.js +1 -9
  75. package/core/server/api/endpoints/labels.js +1 -7
  76. package/core/server/api/endpoints/members.js +3 -13
  77. package/core/server/api/endpoints/offers.js +2 -2
  78. package/core/server/api/endpoints/pages.js +2 -10
  79. package/core/server/api/endpoints/posts.js +1 -9
  80. package/core/server/api/endpoints/settings.js +0 -94
  81. package/core/server/api/endpoints/snippets.js +1 -9
  82. package/core/server/api/endpoints/tags.js +1 -7
  83. package/core/server/api/endpoints/utils/serializers/input/pages.js +1 -1
  84. package/core/server/api/endpoints/utils/serializers/output/members.js +2 -1
  85. package/core/server/api/endpoints/utils/serializers/output/site.js +1 -0
  86. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +6 -7
  87. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -20
  88. package/core/server/api/endpoints/webhooks.js +2 -19
  89. package/core/server/data/migrations/versions/5.11/2022-08-22-11-03-add-member-alert-settings-columns-to-users.js +21 -0
  90. package/core/server/data/migrations/versions/5.11/2022-08-23-13-41-backfill-members-created-events.js +32 -0
  91. package/core/server/data/migrations/versions/5.11/2022-08-23-13-59-fix-page-resource-type.js +22 -0
  92. package/core/server/data/schema/fixtures/fixtures.json +3 -0
  93. package/core/server/data/schema/schema.js +23 -4
  94. package/core/server/lib/image/gravatar.js +8 -7
  95. package/core/server/lib/image/image-size.js +60 -56
  96. package/core/server/models/action.js +6 -19
  97. package/core/server/models/base/plugins/actions.js +26 -3
  98. package/core/server/models/member-created-event.js +10 -2
  99. package/core/server/models/member-paid-subscription-event.js +4 -0
  100. package/core/server/models/member.js +18 -0
  101. package/core/server/models/offer.js +3 -0
  102. package/core/server/models/post.js +2 -3
  103. package/core/server/models/product.js +3 -0
  104. package/core/server/models/settings.js +4 -0
  105. package/core/server/models/subscription-created-event.js +10 -2
  106. package/core/server/models/user.js +41 -7
  107. package/core/server/services/auth/api-key/admin.js +0 -3
  108. package/core/server/services/auth/passwordreset.js +0 -3
  109. package/core/server/services/explore/service.js +7 -6
  110. package/core/server/services/mega/mega.js +7 -4
  111. package/core/server/services/mega/template.js +44 -16
  112. package/core/server/services/member-attribution/index.js +34 -6
  113. package/core/server/services/members/api.js +4 -0
  114. package/core/server/services/members/middleware.js +6 -2
  115. package/core/server/services/members/service.js +6 -3
  116. package/core/server/services/public-config/site.js +1 -0
  117. package/core/server/services/route-settings/default-settings-manager.js +19 -17
  118. package/core/server/services/staff/index.js +26 -0
  119. package/core/server/services/webhooks/trigger.js +14 -5
  120. package/core/server/web/api/endpoints/admin/middleware.js +1 -3
  121. package/core/server/web/api/endpoints/admin/routes.js +0 -7
  122. package/core/shared/config/defaults.json +3 -2
  123. package/core/shared/labs.js +8 -7
  124. package/package.json +86 -84
  125. package/yarn.lock +134 -181
  126. package/components/tryghost-api-framework-5.10.1.tgz +0 -0
  127. package/components/tryghost-api-version-compatibility-service-5.10.1.tgz +0 -0
  128. package/components/tryghost-constants-5.10.1.tgz +0 -0
  129. package/components/tryghost-domain-events-5.10.1.tgz +0 -0
  130. package/components/tryghost-express-dynamic-redirects-5.10.1.tgz +0 -0
  131. package/components/tryghost-extract-api-key-5.10.1.tgz +0 -0
  132. package/components/tryghost-magic-link-5.10.1.tgz +0 -0
  133. package/components/tryghost-mailgun-client-5.10.1.tgz +0 -0
  134. package/components/tryghost-member-analytics-service-5.10.1.tgz +0 -0
  135. package/components/tryghost-member-attribution-5.10.1.tgz +0 -0
  136. package/components/tryghost-member-events-5.10.1.tgz +0 -0
  137. package/components/tryghost-members-api-5.10.1.tgz +0 -0
  138. package/components/tryghost-members-importer-5.10.1.tgz +0 -0
  139. package/components/tryghost-members-offers-5.10.1.tgz +0 -0
  140. package/components/tryghost-members-ssr-5.10.1.tgz +0 -0
  141. package/components/tryghost-minifier-5.10.1.tgz +0 -0
  142. package/components/tryghost-mw-api-version-mismatch-5.10.1.tgz +0 -0
  143. package/components/tryghost-mw-error-handler-5.10.1.tgz +0 -0
  144. package/components/tryghost-mw-update-user-last-seen-5.10.1.tgz +0 -0
  145. package/components/tryghost-mw-vhost-5.10.1.tgz +0 -0
  146. package/components/tryghost-settings-path-manager-5.10.1.tgz +0 -0
  147. package/components/tryghost-verification-trigger-5.10.1.tgz +0 -0
  148. package/core/built/admin/assets/chunk.143.71da0e884297554a8ec4.js +0 -41
  149. package/core/built/admin/assets/ghost-13baab17b3f54b21f341fb8f36f83110.css +0 -1
  150. package/core/built/admin/assets/ghost-dark-b0500577a42e2770994e6aef0e70f182.css +0 -1
@@ -0,0 +1,32 @@
1
+ const ObjectID = require('bson-objectid').default;
2
+ const logging = require('@tryghost/logging');
3
+
4
+ const {createTransactionalMigration} = require('../../utils');
5
+
6
+ module.exports = createTransactionalMigration(
7
+ async function up(knex) {
8
+ const members = await knex('members')
9
+ .select('id', 'created_at');
10
+
11
+ if (members.length === 0) {
12
+ logging.warn(`Skipping migration because no members found`);
13
+ return;
14
+ }
15
+
16
+ const toInsert = members.map((member) => {
17
+ return {
18
+ id: ObjectID().toHexString(),
19
+ member_id: member.id,
20
+ created_at: member.created_at,
21
+ source: 'member'
22
+ };
23
+ });
24
+
25
+ logging.info(`Inserting ${toInsert.length} members created events`);
26
+ await knex.batchInsert('members_created_events', toInsert);
27
+ },
28
+ async function down(knex) {
29
+ logging.info(`Clearing all members created events`);
30
+ await knex('members_created_events').del();
31
+ }
32
+ );
@@ -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(`Changing Action event 'page' resource_type to 'post'`);
7
+
8
+ const affectedRows = await knex('actions')
9
+ .update({
10
+ resource_type: 'post',
11
+ context: JSON.stringify({
12
+ type: 'page'
13
+ })
14
+ })
15
+ .where('resource_type', 'page');
16
+
17
+ logging.info(`Updated ${affectedRows} Action events from 'page' to 'post'`);
18
+ },
19
+ async function down() {
20
+ // no-op: we don't want to put `pages` back as a resource type
21
+ }
22
+ );
@@ -626,6 +626,9 @@
626
626
  "name": "Ghost",
627
627
  "email": "ghost@example.com",
628
628
  "status": "inactive",
629
+ "free_member_signup_notification": true,
630
+ "paid_subscription_started_notification": true,
631
+ "paid_subscription_canceled_notification": false,
629
632
  "roles": []
630
633
  }
631
634
  ]
@@ -149,6 +149,9 @@ module.exports = {
149
149
  tour: {type: 'text', maxlength: 65535, nullable: true},
150
150
  last_seen: {type: 'dateTime', nullable: true},
151
151
  comment_notifications: {type: 'boolean', nullable: false, defaultTo: true},
152
+ free_member_signup_notification: {type: 'boolean', nullable: false, defaultTo: true},
153
+ paid_subscription_started_notification: {type: 'boolean', nullable: false, defaultTo: true},
154
+ paid_subscription_canceled_notification: {type: 'boolean', nullable: false, defaultTo: false},
152
155
  created_at: {type: 'dateTime', nullable: false},
153
156
  created_by: {type: 'string', maxlength: 24, nullable: false},
154
157
  updated_at: {type: 'dateTime', nullable: true},
@@ -480,9 +483,17 @@ module.exports = {
480
483
  created_at: {type: 'dateTime', nullable: false},
481
484
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
482
485
  attribution_id: {type: 'string', maxlength: 24, nullable: true},
483
- attribution_type: {type: 'string', maxlength: 50, nullable: true},
486
+ attribution_type: {
487
+ type: 'string', maxlength: 50, nullable: true, validations: {
488
+ isIn: [['url', 'post', 'page', 'author', 'tag']]
489
+ }
490
+ },
484
491
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
485
- source: {type: 'string', maxlength: 50, nullable: false}
492
+ source: {
493
+ type: 'string', maxlength: 50, nullable: false, validations: {
494
+ isIn: [['member', 'import', 'system', 'api', 'admin']]
495
+ }
496
+ }
486
497
  },
487
498
  members_cancel_events: {
488
499
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -610,7 +621,11 @@ module.exports = {
610
621
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
611
622
  subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
612
623
  attribution_id: {type: 'string', maxlength: 24, nullable: true},
613
- attribution_type: {type: 'string', maxlength: 50, nullable: true},
624
+ attribution_type: {
625
+ type: 'string', maxlength: 50, nullable: true, validations: {
626
+ isIn: [['url', 'post', 'page', 'author', 'tag']]
627
+ }
628
+ },
614
629
  attribution_url: {type: 'string', maxlength: 2000, nullable: true}
615
630
  },
616
631
  offer_redemptions: {
@@ -625,7 +640,11 @@ module.exports = {
625
640
  member_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'members.id', cascadeDelete: true},
626
641
  subscribed: {type: 'bool', nullable: false, defaultTo: true},
627
642
  created_at: {type: 'dateTime', nullable: false},
628
- source: {type: 'string', maxlength: 50, nullable: true},
643
+ source: {
644
+ type: 'string', maxlength: 50, nullable: true, validations: {
645
+ isIn: [['member', 'import', 'system', 'api', 'admin']]
646
+ }
647
+ },
629
648
  newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id', cascadeDelete: false}
630
649
  },
631
650
  stripe_products: {
@@ -40,15 +40,16 @@ class Gravatar {
40
40
  image: imageUrl
41
41
  };
42
42
  })
43
- .catch({statusCode: 404}, function () {
44
- return {
45
- image: undefined
46
- };
47
- })
48
- .catch(function () {
43
+ .catch(function (err) {
44
+ if (err.statusCode === 404) {
45
+ return {
46
+ image: undefined
47
+ };
48
+ }
49
+
49
50
  // ignore error, just resolve with no image url
50
51
  });
51
52
  }
52
53
  }
53
54
 
54
- module.exports = Gravatar;
55
+ module.exports = Gravatar;
@@ -163,39 +163,41 @@ class ImageSize {
163
163
  width: dimensions.width,
164
164
  height: dimensions.height
165
165
  };
166
- }).catch({code: 'URL_MISSING_INVALID'}, (err) => {
167
- return Promise.reject(new errors.InternalServerError({
168
- message: err.message,
169
- code: 'IMAGE_SIZE_URL',
170
- statusCode: err.statusCode,
171
- context: err.url || imagePath
172
- }));
173
- }).catch({code: 'ETIMEDOUT'}, {code: 'ESOCKETTIMEDOUT'}, {code: 'ECONNRESET'}, {statusCode: 408}, (err) => {
174
- return Promise.reject(new errors.InternalServerError({
175
- message: 'Request timed out.',
176
- code: 'IMAGE_SIZE_URL',
177
- statusCode: err.statusCode,
178
- context: err.url || imagePath
179
- }));
180
- }).catch({code: 'ENOENT'}, {code: 'ENOTFOUND'}, {statusCode: 404}, (err) => {
181
- return Promise.reject(new errors.NotFoundError({
182
- message: 'Image not found.',
183
- code: 'IMAGE_SIZE_URL',
184
- statusCode: err.statusCode,
185
- context: err.url || imagePath
186
- }));
187
- }).catch(function (err) {
188
- if (errors.utils.isGhostError(err)) {
189
- return Promise.reject(err);
190
- }
166
+ }).catch((err) => {
167
+ if (err.code === 'URL_MISSING_INVALID') {
168
+ return Promise.reject(new errors.InternalServerError({
169
+ message: err.message,
170
+ code: 'IMAGE_SIZE_URL',
171
+ statusCode: err.statusCode,
172
+ context: err.url || imagePath
173
+ }));
174
+ } else if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT' || err.code === 'ECONNRESET' || err.statusCode === 408) {
175
+ return Promise.reject(new errors.InternalServerError({
176
+ message: 'Request timed out.',
177
+ code: 'IMAGE_SIZE_URL',
178
+ statusCode: err.statusCode,
179
+ context: err.url || imagePath
180
+ }));
181
+ } else if (err.code === 'ENOENT' || err.code === 'ENOTFOUND' || err.statusCode === 404) {
182
+ return Promise.reject(new errors.NotFoundError({
183
+ message: 'Image not found.',
184
+ code: 'IMAGE_SIZE_URL',
185
+ statusCode: err.statusCode,
186
+ context: err.url || imagePath
187
+ }));
188
+ } else {
189
+ if (errors.utils.isGhostError(err)) {
190
+ return Promise.reject(err);
191
+ }
191
192
 
192
- return Promise.reject(new errors.InternalServerError({
193
- message: 'Unknown Request error.',
194
- code: 'IMAGE_SIZE_URL',
195
- statusCode: err.statusCode,
196
- context: err.url || imagePath,
197
- err: err
198
- }));
193
+ return Promise.reject(new errors.InternalServerError({
194
+ message: 'Unknown Request error.',
195
+ code: 'IMAGE_SIZE_URL',
196
+ statusCode: err.statusCode,
197
+ context: err.url || imagePath,
198
+ err: err
199
+ }));
200
+ }
199
201
  });
200
202
  }
201
203
 
@@ -237,32 +239,34 @@ class ImageSize {
237
239
  height: dimensions.height
238
240
  };
239
241
  })
240
- .catch({code: 'ENOENT'}, (err) => {
241
- return Promise.reject(new errors.NotFoundError({
242
- message: err.message,
243
- code: 'IMAGE_SIZE_STORAGE',
244
- err: err,
245
- context: filePath,
246
- errorDetails: {
247
- originalPath: imagePath,
248
- reqFilePath: filePath
242
+ .catch((err) => {
243
+ if (err.code === 'ENOENT') {
244
+ return Promise.reject(new errors.NotFoundError({
245
+ message: err.message,
246
+ code: 'IMAGE_SIZE_STORAGE',
247
+ err: err,
248
+ context: filePath,
249
+ errorDetails: {
250
+ originalPath: imagePath,
251
+ reqFilePath: filePath
252
+ }
253
+ }));
254
+ } else {
255
+ if (errors.utils.isGhostError(err)) {
256
+ return Promise.reject(err);
249
257
  }
250
- }));
251
- }).catch((err) => {
252
- if (errors.utils.isGhostError(err)) {
253
- return Promise.reject(err);
254
- }
255
258
 
256
- return Promise.reject(new errors.InternalServerError({
257
- message: err.message,
258
- code: 'IMAGE_SIZE_STORAGE',
259
- err: err,
260
- context: filePath,
261
- errorDetails: {
262
- originalPath: imagePath,
263
- reqFilePath: filePath
264
- }
265
- }));
259
+ return Promise.reject(new errors.InternalServerError({
260
+ message: err.message,
261
+ code: 'IMAGE_SIZE_STORAGE',
262
+ err: err,
263
+ context: filePath,
264
+ errorDetails: {
265
+ originalPath: imagePath,
266
+ reqFilePath: filePath
267
+ }
268
+ }));
269
+ }
266
270
  });
267
271
  }
268
272
 
@@ -1,34 +1,21 @@
1
- const _ = require('lodash');
2
1
  const ghostBookshelf = require('./base');
3
2
 
4
- const candidates = [];
5
-
6
3
  const Action = ghostBookshelf.Model.extend({
7
4
  tableName: 'actions',
8
5
 
9
- initialize: function initialize() {
10
- _.each(ghostBookshelf.registry.models, (model) => {
11
- candidates.push([model, model.prototype.tableName.replace(/s$/, '')]);
6
+ candidates() {
7
+ return Object.keys(ghostBookshelf.registry.models).map((key) => {
8
+ const model = ghostBookshelf.registry.models[key];
9
+ return [model, model.prototype.tableName.replace(/s$/, '')];
12
10
  });
13
- this.constructor.__super__.initialize.apply(this, arguments);
14
11
  },
15
12
 
16
13
  actor() {
17
- return this.morphTo('actor', ['actor_type', 'actor_id'], ...candidates);
14
+ return this.morphTo('actor', ['actor_type', 'actor_id'], ...this.candidates());
18
15
  },
19
16
 
20
17
  resource() {
21
- return this.morphTo('resource', ['resource_type', 'resource_id'], ...candidates);
22
- },
23
-
24
- toJSON(unfilteredOptions) {
25
- const options = Action.filterOptions(unfilteredOptions, 'toJSON');
26
- const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
27
-
28
- // @TODO: context is not implemented yet
29
- delete attrs.context;
30
-
31
- return attrs;
18
+ return this.morphTo('resource', ['resource_type', 'resource_id'], ...this.candidates());
32
19
  }
33
20
  }, {
34
21
  orderDefaultOptions: function orderDefaultOptions() {
@@ -29,14 +29,37 @@ module.exports = function (Bookshelf) {
29
29
  resourceType = resourceType.bind(this)();
30
30
  }
31
31
 
32
- // @TODO: implement context
33
- return {
34
- event: event,
32
+ if (!resourceType) {
33
+ return;
34
+ }
35
+
36
+ let context = {};
37
+
38
+ if (this.actionsExtraContext && Array.isArray(this.actionsExtraContext)) {
39
+ for (const c of this.actionsExtraContext) {
40
+ context[c] = this.get(c) || this.previous(c);
41
+ }
42
+ }
43
+
44
+ if (event === 'deleted') {
45
+ context.primary_name = (this.previous('title') || this.previous('name'));
46
+ } else if (['added', 'edited'].includes(event)) {
47
+ context.primary_name = (this.get('title') || this.get('name') || this.previous('title') || this.previous('name'));
48
+ }
49
+
50
+ const data = {
51
+ event,
35
52
  resource_id: this.id || this.previous('id'),
36
53
  resource_type: resourceType,
37
54
  actor_id: actor.id,
38
55
  actor_type: actor.type
39
56
  };
57
+
58
+ if (context && Object.keys(context).length) {
59
+ data.context = context;
60
+ }
61
+
62
+ return data;
40
63
  },
41
64
 
42
65
  /**
@@ -8,8 +8,16 @@ const MemberCreatedEvent = ghostBookshelf.Model.extend({
8
8
  return this.belongsTo('Member', 'member_id', 'id');
9
9
  },
10
10
 
11
- attribution() {
12
- return this.belongsTo('Post', 'attribution_id', 'id');
11
+ postAttribution() {
12
+ return this.belongsTo('Post', 'attribution_id', 'id');
13
+ },
14
+
15
+ userAttribution() {
16
+ return this.belongsTo('User', 'attribution_id', 'id');
17
+ },
18
+
19
+ tagAttribution() {
20
+ return this.belongsTo('Tag', 'attribution_id', 'id');
13
21
  }
14
22
  }, {
15
23
  async edit() {
@@ -8,6 +8,10 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
8
8
  return this.belongsTo('Member', 'member_id', 'id');
9
9
  },
10
10
 
11
+ subscriptionCreatedEvent() {
12
+ return this.belongsTo('SubscriptionCreatedEvent', 'subscription_id', 'subscription_id');
13
+ },
14
+
11
15
  customQuery(qb, options) {
12
16
  if (options.aggregateMRRDeltas) {
13
17
  if (options.limit || options.filter) {
@@ -40,6 +40,12 @@ const Member = ghostBookshelf.Model.extend({
40
40
  }, {
41
41
  key: 'newsletters',
42
42
  replacement: 'newsletters.slug'
43
+ }, {
44
+ key: 'signup',
45
+ replacement: 'signups.attribution_id'
46
+ }, {
47
+ key: 'conversion',
48
+ replacement: 'conversions.attribution_id'
43
49
  }];
44
50
  },
45
51
 
@@ -74,6 +80,18 @@ const Member = ghostBookshelf.Model.extend({
74
80
  joinFrom: 'member_id',
75
81
  joinTo: 'customer_id',
76
82
  joinToForeign: 'customer_id'
83
+ },
84
+ signups: {
85
+ tableName: 'members_created_events',
86
+ tableNameAs: 'signups',
87
+ type: 'oneToOne',
88
+ joinFrom: 'member_id'
89
+ },
90
+ conversions: {
91
+ tableName: 'members_subscription_created_events',
92
+ tableNameAs: 'conversions',
93
+ type: 'oneToOne',
94
+ joinFrom: 'member_id'
77
95
  }
78
96
  };
79
97
  },
@@ -3,6 +3,9 @@ const ghostBookshelf = require('./base');
3
3
  const Offer = ghostBookshelf.Model.extend({
4
4
  tableName: 'offers',
5
5
 
6
+ actionsCollectCRUD: true,
7
+ actionsResourceType: 'offer',
8
+
6
9
  product() {
7
10
  return this.belongsTo('Product', 'product_id', 'id');
8
11
  }
@@ -39,9 +39,8 @@ Post = ghostBookshelf.Model.extend({
39
39
  tableName: 'posts',
40
40
 
41
41
  actionsCollectCRUD: true,
42
- actionsResourceType: function () {
43
- return this.get('type') || this.previous('type');
44
- },
42
+ actionsResourceType: 'post',
43
+ actionsExtraContext: ['type'],
45
44
 
46
45
  /**
47
46
  * @NOTE
@@ -4,6 +4,9 @@ const _ = require('lodash');
4
4
  const Product = ghostBookshelf.Model.extend({
5
5
  tableName: 'products',
6
6
 
7
+ actionsCollectCRUD: true,
8
+ actionsResourceType: 'product',
9
+
7
10
  defaults: {
8
11
  active: true,
9
12
  visibility: 'none',
@@ -97,6 +97,10 @@ Settings = ghostBookshelf.Model.extend({
97
97
 
98
98
  tableName: 'settings',
99
99
 
100
+ actionsCollectCRUD: true,
101
+ actionsResourceType: 'setting',
102
+ actionsExtraContext: ['key', 'group'],
103
+
100
104
  emitChange: function emitChange(event, options) {
101
105
  const eventToTrigger = 'settings' + '.' + event;
102
106
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
@@ -12,8 +12,16 @@ const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
12
12
  return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
13
13
  },
14
14
 
15
- attribution() {
16
- return this.belongsTo('Post', 'attribution_id', 'id');
15
+ postAttribution() {
16
+ return this.belongsTo('Post', 'attribution_id', 'id');
17
+ },
18
+
19
+ userAttribution() {
20
+ return this.belongsTo('User', 'attribution_id', 'id');
21
+ },
22
+
23
+ tagAttribution() {
24
+ return this.belongsTo('Tag', 'attribution_id', 'id');
17
25
  }
18
26
  }, {
19
27
  async edit() {
@@ -65,7 +65,10 @@ User = ghostBookshelf.Model.extend({
65
65
  password: security.identifier.uid(50),
66
66
  visibility: 'public',
67
67
  status: 'active',
68
- comment_notifications: true
68
+ comment_notifications: true,
69
+ free_member_signup_notification: true,
70
+ paid_subscription_started_notification: true,
71
+ paid_subscription_canceled_notification: false
69
72
  };
70
73
  },
71
74
 
@@ -485,6 +488,33 @@ User = ghostBookshelf.Model.extend({
485
488
  return query.fetch(options);
486
489
  },
487
490
 
491
+ /**
492
+ * Returns users who should receive a specific type of alert
493
+ * @param {'free-signup'|'paid-started'|'paid-canceled'} type The type of alert to fetch users for
494
+ * @param {any} options
495
+ * @return {Promise<[Object]>} Array of users
496
+ */
497
+ getEmailAlertUsers(type, options) {
498
+ options = options || {};
499
+
500
+ let filter = 'status:active';
501
+ if (type === 'free-signup') {
502
+ filter += '+free_member_signup_notification:true';
503
+ } else if (type === 'paid-started') {
504
+ filter += '+paid_subscription_started_notification:true';
505
+ } else if (type === 'paid-canceled') {
506
+ filter += '+paid_subscription_canceled_notification:true';
507
+ }
508
+ const updatedOptions = _.merge({}, options, {filter, withRelated: ['roles']});
509
+ return this.findAll(updatedOptions).then((users) => {
510
+ return users.toJSON().filter((user) => {
511
+ return user?.roles?.some((role) => {
512
+ return ['Owner', 'Administrator'].includes(role.name);
513
+ });
514
+ });
515
+ });
516
+ },
517
+
488
518
  /**
489
519
  * ### Edit
490
520
  *
@@ -1002,10 +1032,10 @@ User = ghostBookshelf.Model.extend({
1002
1032
  let ownerRole;
1003
1033
  let contextUser;
1004
1034
 
1005
- return Promise.join(
1035
+ return Promise.all([
1006
1036
  ghostBookshelf.model('Role').findOne({name: 'Owner'}),
1007
1037
  User.findOne({id: options.context.user}, {withRelated: ['roles']})
1008
- )
1038
+ ])
1009
1039
  .then((results) => {
1010
1040
  ownerRole = results[0];
1011
1041
  contextUser = results[1];
@@ -1018,8 +1048,10 @@ User = ghostBookshelf.Model.extend({
1018
1048
  }));
1019
1049
  }
1020
1050
 
1021
- return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Administrator'}),
1022
- User.findOne({id: object.id}, {withRelated: ['roles']}));
1051
+ return Promise.all([
1052
+ ghostBookshelf.model('Role').findOne({name: 'Administrator'}),
1053
+ User.findOne({id: object.id}, {withRelated: ['roles']})
1054
+ ]);
1023
1055
  })
1024
1056
  .then((results) => {
1025
1057
  const adminRole = results[0];
@@ -1046,9 +1078,11 @@ User = ghostBookshelf.Model.extend({
1046
1078
  }
1047
1079
 
1048
1080
  // convert owner to admin
1049
- return Promise.join(contextUser.roles().updatePivot({role_id: adminRole.id}),
1081
+ return Promise.all([
1082
+ contextUser.roles().updatePivot({role_id: adminRole.id}),
1050
1083
  user.roles().updatePivot({role_id: ownerRole.id}),
1051
- user.id);
1084
+ user.id
1085
+ ]);
1052
1086
  })
1053
1087
  .then((results) => {
1054
1088
  return Users.forge()
@@ -181,9 +181,6 @@ const authenticateWithToken = async (req, res, next, {token, JWT_OPTIONS}) => {
181
181
  );
182
182
 
183
183
  req.user = user;
184
-
185
- next();
186
- return;
187
184
  }
188
185
 
189
186
  // store the api key on the request for later checks and logging
@@ -144,9 +144,6 @@ function doReset(options, tokenParts, settingsAPI) {
144
144
  updatedUser.set('status', 'active');
145
145
  return updatedUser.save(options);
146
146
  })
147
- .catch(errors.ValidationError, (err) => {
148
- return Promise.reject(err);
149
- })
150
147
  .catch((err) => {
151
148
  if (errors.utils.isGhostError(err)) {
152
149
  return Promise.reject(err);
@@ -26,18 +26,19 @@ module.exports = class ExploreService {
26
26
  const totalMembers = await this.MembersService.stats.getTotalMembers();
27
27
  const mrrStats = await this.StatsService.getMRRHistory();
28
28
 
29
- const {description, icon, title, url, accent_color: accentColor} = this.PublicConfigService.site;
29
+ const {description, icon, title, url, accent_color: accentColor, locale} = this.PublicConfigService.site;
30
30
 
31
31
  const exploreProperties = {
32
32
  version: ghostVersion.full,
33
- totalMembers,
34
- mrrStats,
33
+ total_members: totalMembers,
34
+ mrr_stats: mrrStats,
35
35
  site: {
36
36
  description,
37
37
  icon,
38
38
  title,
39
39
  url,
40
- accentColor
40
+ accent_color: accentColor,
41
+ locale
41
42
  },
42
43
  stripe: {
43
44
  configured: this.StripeService.api.configured,
@@ -46,10 +47,10 @@ module.exports = class ExploreService {
46
47
  };
47
48
 
48
49
  const mostRecentlyPublishedPost = await this.PostsService.getMostRecentlyPublishedPost();
49
- exploreProperties.mostRecentlyPublishedAt = mostRecentlyPublishedPost?.get('published_at') || null;
50
+ exploreProperties.most_recently_published_at = mostRecentlyPublishedPost?.get('published_at') || null;
50
51
 
51
52
  const owner = await this.UserModel.findOne({role: 'Owner', status: 'all'});
52
- exploreProperties.ownerEmail = owner?.get('email') || null;
53
+ exploreProperties.owner_email = owner?.get('email') || null;
53
54
 
54
55
  return exploreProperties;
55
56
  }