ghost 5.10.0 → 5.12.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 (161) hide show
  1. package/components/tryghost-adapter-manager-5.12.0.tgz +0 -0
  2. package/components/tryghost-api-framework-5.12.0.tgz +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-0.0.0.tgz → tryghost-api-version-compatibility-service-5.12.0.tgz} +0 -0
  4. package/components/tryghost-bootstrap-socket-5.12.0.tgz +0 -0
  5. package/components/tryghost-constants-5.12.0.tgz +0 -0
  6. package/components/tryghost-custom-theme-settings-service-5.12.0.tgz +0 -0
  7. package/components/tryghost-domain-events-5.12.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-5.12.0.tgz +0 -0
  9. package/components/tryghost-email-analytics-service-5.12.0.tgz +0 -0
  10. package/components/tryghost-email-content-generator-5.12.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.12.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.12.0.tgz +0 -0
  13. package/components/{tryghost-html-to-plaintext-0.0.0.tgz → tryghost-html-to-plaintext-5.12.0.tgz} +0 -0
  14. package/components/tryghost-job-manager-5.12.0.tgz +0 -0
  15. package/components/tryghost-magic-link-5.12.0.tgz +0 -0
  16. package/components/tryghost-mailgun-client-5.12.0.tgz +0 -0
  17. package/components/tryghost-member-analytics-service-5.12.0.tgz +0 -0
  18. package/components/tryghost-member-attribution-5.12.0.tgz +0 -0
  19. package/components/tryghost-member-events-5.12.0.tgz +0 -0
  20. package/components/tryghost-members-analytics-ingress-5.12.0.tgz +0 -0
  21. package/components/tryghost-members-api-5.12.0.tgz +0 -0
  22. package/components/{tryghost-members-csv-0.0.0.tgz → tryghost-members-csv-5.12.0.tgz} +0 -0
  23. package/components/tryghost-members-events-service-5.12.0.tgz +0 -0
  24. package/components/tryghost-members-importer-5.12.0.tgz +0 -0
  25. package/components/tryghost-members-offers-5.12.0.tgz +0 -0
  26. package/components/tryghost-members-payments-5.12.0.tgz +0 -0
  27. package/components/tryghost-members-ssr-5.12.0.tgz +0 -0
  28. package/components/tryghost-members-stripe-service-5.12.0.tgz +0 -0
  29. package/components/tryghost-minifier-5.12.0.tgz +0 -0
  30. package/components/tryghost-mw-api-version-mismatch-5.12.0.tgz +0 -0
  31. package/components/tryghost-mw-cache-control-5.12.0.tgz +0 -0
  32. package/components/tryghost-mw-error-handler-5.12.0.tgz +0 -0
  33. package/components/tryghost-mw-session-from-token-5.12.0.tgz +0 -0
  34. package/components/tryghost-mw-update-user-last-seen-5.12.0.tgz +0 -0
  35. package/components/tryghost-mw-vhost-5.12.0.tgz +0 -0
  36. package/components/tryghost-oembed-service-5.12.0.tgz +0 -0
  37. package/components/tryghost-package-json-5.12.0.tgz +0 -0
  38. package/components/tryghost-security-5.12.0.tgz +0 -0
  39. package/components/tryghost-session-service-5.12.0.tgz +0 -0
  40. package/components/tryghost-settings-path-manager-5.12.0.tgz +0 -0
  41. package/components/tryghost-staff-service-5.12.0.tgz +0 -0
  42. package/components/tryghost-update-check-service-5.12.0.tgz +0 -0
  43. package/components/tryghost-verification-trigger-5.12.0.tgz +0 -0
  44. package/components/tryghost-version-notifications-data-service-5.12.0.tgz +0 -0
  45. package/core/boot.js +2 -0
  46. package/core/built/admin/assets/chunk.143.da85cf08f47cfe520bf6.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.8a19c35ce1a7cf4249ce.js → chunk.178.29d3fb3dc6811b673a00.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-dark-e4d6987343ee26af534a06c1b9639d97.css +1 -0
  53. package/core/built/admin/assets/ghost-f62b0e78ddcd947273873bdeba4abc3c.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/snippets.js +1 -9
  81. package/core/server/api/endpoints/tags.js +1 -7
  82. package/core/server/api/endpoints/utils/serializers/input/pages.js +1 -1
  83. package/core/server/api/endpoints/utils/serializers/output/members.js +2 -1
  84. package/core/server/api/endpoints/utils/serializers/output/site.js +1 -0
  85. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +6 -7
  86. package/core/server/api/endpoints/webhooks.js +2 -19
  87. package/core/server/data/migrations/versions/5.11/2022-08-22-11-03-add-member-alert-settings-columns-to-users.js +21 -0
  88. package/core/server/data/migrations/versions/5.11/2022-08-23-13-41-backfill-members-created-events.js +32 -0
  89. package/core/server/data/migrations/versions/5.11/2022-08-23-13-59-fix-page-resource-type.js +22 -0
  90. package/core/server/data/schema/fixtures/fixtures.json +3 -0
  91. package/core/server/data/schema/schema.js +23 -4
  92. package/core/server/lib/image/gravatar.js +8 -7
  93. package/core/server/lib/image/image-size.js +60 -56
  94. package/core/server/models/action.js +6 -19
  95. package/core/server/models/base/plugins/actions.js +26 -3
  96. package/core/server/models/member-created-event.js +10 -2
  97. package/core/server/models/member-paid-subscription-event.js +4 -0
  98. package/core/server/models/member.js +18 -0
  99. package/core/server/models/offer.js +3 -0
  100. package/core/server/models/post.js +2 -3
  101. package/core/server/models/product.js +3 -0
  102. package/core/server/models/settings.js +4 -0
  103. package/core/server/models/subscription-created-event.js +10 -2
  104. package/core/server/models/user.js +41 -7
  105. package/core/server/services/auth/api-key/admin.js +0 -3
  106. package/core/server/services/auth/passwordreset.js +0 -3
  107. package/core/server/services/explore/service.js +7 -6
  108. package/core/server/services/member-attribution/index.js +34 -6
  109. package/core/server/services/members/api.js +4 -0
  110. package/core/server/services/members/service.js +6 -3
  111. package/core/server/services/public-config/site.js +1 -0
  112. package/core/server/services/route-settings/default-settings-manager.js +19 -17
  113. package/core/server/services/staff/index.js +26 -0
  114. package/core/server/services/webhooks/trigger.js +14 -5
  115. package/core/shared/config/defaults.json +3 -2
  116. package/core/shared/labs.js +8 -7
  117. package/package.json +84 -82
  118. package/yarn.lock +101 -123
  119. package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
  120. package/components/tryghost-api-framework-0.0.0.tgz +0 -0
  121. package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
  122. package/components/tryghost-constants-0.0.0.tgz +0 -0
  123. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  124. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  125. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  126. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  127. package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
  128. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  129. package/components/tryghost-extract-api-key-0.0.0.tgz +0 -0
  130. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  131. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  132. package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
  133. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  134. package/components/tryghost-member-attribution-0.0.0.tgz +0 -0
  135. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  136. package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
  137. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  138. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  139. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  140. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  141. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  142. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  143. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  144. package/components/tryghost-minifier-0.0.0.tgz +0 -0
  145. package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
  146. package/components/tryghost-mw-cache-control-0.0.0.tgz +0 -0
  147. package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
  148. package/components/tryghost-mw-session-from-token-0.0.0.tgz +0 -0
  149. package/components/tryghost-mw-update-user-last-seen-0.0.0.tgz +0 -0
  150. package/components/tryghost-mw-vhost-0.0.0.tgz +0 -0
  151. package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
  152. package/components/tryghost-package-json-0.0.0.tgz +0 -0
  153. package/components/tryghost-security-0.0.0.tgz +0 -0
  154. package/components/tryghost-session-service-0.0.0.tgz +0 -0
  155. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  156. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  157. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  158. package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
  159. package/core/built/admin/assets/chunk.143.6a3c46a89c731b86a730.js +0 -41
  160. package/core/built/admin/assets/ghost-13baab17b3f54b21f341fb8f36f83110.css +0 -1
  161. package/core/built/admin/assets/ghost-dark-b0500577a42e2770994e6aef0e70f182.css +0 -1
@@ -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
  }
@@ -1,23 +1,51 @@
1
1
  const urlService = require('../url');
2
2
  const labsService = require('../../../shared/labs');
3
+ const DomainEvents = require('@tryghost/domain-events');
4
+ const urlUtils = require('../../../shared/url-utils');
3
5
 
4
6
  class MemberAttributionServiceWrapper {
5
7
  init() {
6
- if (this.service) {
8
+ if (this.eventHandler) {
7
9
  // Prevent creating duplicate DomainEvents subscribers
8
10
  return;
9
11
  }
10
12
 
11
- const MemberAttributionService = require('@tryghost/member-attribution');
13
+ // Wire up all the dependencies
14
+ const {MemberAttributionService, UrlTranslator, AttributionBuilder, EventHandler} = require('@tryghost/member-attribution');
12
15
  const models = require('../../models');
13
16
 
14
- // For now we don't need to expose anything (yet)
17
+ const urlTranslator = new UrlTranslator({
18
+ urlService,
19
+ urlUtils,
20
+ models: {
21
+ Post: models.Post,
22
+ User: models.User,
23
+ Tag: models.Tag
24
+ }
25
+ });
26
+
27
+ this.attributionBuilder = new AttributionBuilder({urlTranslator});
28
+
29
+ // Expose the service
15
30
  this.service = new MemberAttributionService({
16
- MemberCreatedEvent: models.MemberCreatedEvent,
17
- SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
18
- urlService,
31
+ models: {
32
+ MemberCreatedEvent: models.MemberCreatedEvent,
33
+ SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
34
+ },
35
+ attributionBuilder: this.attributionBuilder,
36
+ labsService
37
+ });
38
+
39
+ // Listen for events and store them in the database
40
+ this.eventHandler = new EventHandler({
41
+ models: {
42
+ MemberCreatedEvent: models.MemberCreatedEvent,
43
+ SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
44
+ },
45
+ DomainEvents,
19
46
  labsService
20
47
  });
48
+ this.eventHandler.subscribe();
21
49
  }
22
50
  }
23
51
 
@@ -13,6 +13,7 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
13
13
  const urlUtils = require('../../../shared/url-utils');
14
14
  const labsService = require('../../../shared/labs');
15
15
  const offersService = require('../offers');
16
+ const staffService = require('../staff');
16
17
  const newslettersService = require('../newsletters');
17
18
  const memberAttributionService = require('../member-attribution');
18
19
 
@@ -185,6 +186,8 @@ function createApiInstance(config) {
185
186
  MemberStatusEvent: models.MemberStatusEvent,
186
187
  MemberProductEvent: models.MemberProductEvent,
187
188
  MemberAnalyticEvent: models.MemberAnalyticEvent,
189
+ MemberCreatedEvent: models.MemberCreatedEvent,
190
+ SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
188
191
  OfferRedemption: models.OfferRedemption,
189
192
  Offer: models.Offer,
190
193
  StripeProduct: models.StripeProduct,
@@ -195,6 +198,7 @@ function createApiInstance(config) {
195
198
  },
196
199
  stripeAPIService: stripeService.api,
197
200
  offersAPI: offersService.api,
201
+ staffService: staffService.api,
198
202
  labsService: labsService,
199
203
  newslettersService: newslettersService,
200
204
  memberAttributionService: memberAttributionService.service
@@ -105,10 +105,12 @@ module.exports = {
105
105
  });
106
106
 
107
107
  verificationTrigger = new VerificationTrigger({
108
- configThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
108
+ apiTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.apiThreshold'),
109
+ adminTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.adminThreshold'),
110
+ importTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
109
111
  isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
110
112
  isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
111
- sendVerificationEmail: ({subject, message, amountImported}) => {
113
+ sendVerificationEmail: ({subject, message, amountTriggered}) => {
112
114
  const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
113
115
  const fromAddress = config.get('user_email');
114
116
 
@@ -116,7 +118,7 @@ module.exports = {
116
118
  ghostMailer.send({
117
119
  subject,
118
120
  html: tpl(message, {
119
- importedNumber: amountImported,
121
+ amountTriggered: amountTriggered,
120
122
  siteUrl: urlUtils.getSiteUrl()
121
123
  }),
122
124
  forceTextContent: true,
@@ -189,4 +191,5 @@ module.exports = {
189
191
  stats: membersStats,
190
192
  export: require('./exporter/query')
191
193
  };
194
+
192
195
  module.exports.middleware = require('./middleware');
@@ -10,6 +10,7 @@ module.exports = function getSiteProperties() {
10
10
  logo: settingsCache.get('logo'),
11
11
  icon: settingsCache.get('icon'),
12
12
  accent_color: settingsCache.get('accent_color'),
13
+ locale: settingsCache.get('locale'),
13
14
  url: urlUtils.urlFor('home', true),
14
15
  version: ghostVersion.safe
15
16
  };