ghost 5.9.4 → 5.11.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 (178) hide show
  1. package/components/tryghost-adapter-manager-5.11.0.tgz +0 -0
  2. package/components/tryghost-api-framework-5.11.0.tgz +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-0.0.0.tgz → tryghost-api-version-compatibility-service-5.11.0.tgz} +0 -0
  4. package/components/tryghost-bootstrap-socket-5.11.0.tgz +0 -0
  5. package/components/tryghost-constants-5.11.0.tgz +0 -0
  6. package/components/tryghost-custom-theme-settings-service-5.11.0.tgz +0 -0
  7. package/components/tryghost-domain-events-5.11.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-5.11.0.tgz +0 -0
  9. package/components/tryghost-email-analytics-service-5.11.0.tgz +0 -0
  10. package/components/tryghost-email-content-generator-5.11.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.11.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.11.0.tgz +0 -0
  13. package/components/tryghost-html-to-plaintext-5.11.0.tgz +0 -0
  14. package/components/tryghost-job-manager-5.11.0.tgz +0 -0
  15. package/components/tryghost-magic-link-5.11.0.tgz +0 -0
  16. package/components/tryghost-mailgun-client-5.11.0.tgz +0 -0
  17. package/components/tryghost-member-analytics-service-5.11.0.tgz +0 -0
  18. package/components/tryghost-member-attribution-5.11.0.tgz +0 -0
  19. package/components/tryghost-member-events-5.11.0.tgz +0 -0
  20. package/components/tryghost-members-analytics-ingress-5.11.0.tgz +0 -0
  21. package/components/tryghost-members-api-5.11.0.tgz +0 -0
  22. package/components/{tryghost-members-csv-0.0.0.tgz → tryghost-members-csv-5.11.0.tgz} +0 -0
  23. package/components/tryghost-members-events-service-5.11.0.tgz +0 -0
  24. package/components/tryghost-members-importer-5.11.0.tgz +0 -0
  25. package/components/tryghost-members-offers-5.11.0.tgz +0 -0
  26. package/components/tryghost-members-payments-5.11.0.tgz +0 -0
  27. package/components/tryghost-members-ssr-5.11.0.tgz +0 -0
  28. package/components/tryghost-members-stripe-service-5.11.0.tgz +0 -0
  29. package/components/tryghost-minifier-5.11.0.tgz +0 -0
  30. package/components/tryghost-mw-api-version-mismatch-5.11.0.tgz +0 -0
  31. package/components/tryghost-mw-cache-control-5.11.0.tgz +0 -0
  32. package/components/tryghost-mw-error-handler-5.11.0.tgz +0 -0
  33. package/components/tryghost-mw-session-from-token-5.11.0.tgz +0 -0
  34. package/components/tryghost-mw-update-user-last-seen-5.11.0.tgz +0 -0
  35. package/components/tryghost-mw-vhost-5.11.0.tgz +0 -0
  36. package/components/tryghost-oembed-service-5.11.0.tgz +0 -0
  37. package/components/tryghost-package-json-5.11.0.tgz +0 -0
  38. package/components/tryghost-security-5.11.0.tgz +0 -0
  39. package/components/tryghost-session-service-5.11.0.tgz +0 -0
  40. package/components/tryghost-settings-path-manager-5.11.0.tgz +0 -0
  41. package/components/tryghost-update-check-service-5.11.0.tgz +0 -0
  42. package/components/tryghost-verification-trigger-5.11.0.tgz +0 -0
  43. package/components/tryghost-version-notifications-data-service-5.11.0.tgz +0 -0
  44. package/content/themes/casper/assets/built/screen.css +1 -1
  45. package/content/themes/casper/assets/built/screen.css.map +1 -1
  46. package/content/themes/casper/assets/css/screen.css +8 -5
  47. package/content/themes/casper/package.json +1 -1
  48. package/core/boot.js +2 -0
  49. package/core/bridge.js +2 -0
  50. package/core/built/admin/assets/chunk.143.14589cc066b8120b73e3.js +49 -0
  51. package/core/built/admin/assets/{chunk.174.eec7f6398cef4c3e2485.js → chunk.174.ae492405065373dbe102.js} +31 -29
  52. package/core/built/admin/assets/{chunk.178.506264293194a4922091.js → chunk.178.131e85a10d2031148425.js} +4 -4
  53. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js → chunk.579.65e09dd89eec70d059a0.js} +23 -28
  54. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js.LICENSE.txt → chunk.579.65e09dd89eec70d059a0.js.LICENSE.txt} +0 -0
  55. package/core/built/admin/assets/ghost-1b0d7c731511bb738ec457d2932c43c0.css +1 -0
  56. package/core/built/admin/assets/{ghost-b441c9cfa2e31453e86460e50ae7e378.js → ghost-40f5bd12d121c54bbc39e7939e78244f.js} +827 -611
  57. package/core/built/admin/assets/ghost-dark-7b2825a050b0382630180f48aa78ea5d.css +1 -0
  58. package/core/built/admin/assets/icons/calendar-stroke.svg +1 -0
  59. package/core/built/admin/assets/icons/ghost-orb-pink.svg +10 -0
  60. package/core/built/admin/assets/icons/pen-stroke.svg +1 -0
  61. package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
  62. package/core/built/admin/assets/{vendor-516c9e43b4aeb92079dc1ab92c9ce492.js → vendor-741dc0e4078e044a0c9bfaad104de8b3.js} +85 -78
  63. package/core/built/admin/index.html +6 -6
  64. package/core/frontend/helpers/ghost_head.js +4 -0
  65. package/core/frontend/helpers/search.js +42 -0
  66. package/core/frontend/services/member-attribution-assets/index.js +4 -0
  67. package/core/frontend/services/member-attribution-assets/service.js +83 -0
  68. package/core/frontend/src/member-attribution/.eslintrc +10 -0
  69. package/core/frontend/src/member-attribution/member-attribution.js +90 -0
  70. package/core/frontend/web/site.js +3 -0
  71. package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +7 -0
  72. package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +7 -0
  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 +11 -10
  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/input/posts.js +1 -1
  84. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -0
  85. package/core/server/api/endpoints/utils/serializers/output/members.js +2 -1
  86. package/core/server/api/endpoints/utils/serializers/output/site.js +1 -0
  87. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +6 -7
  88. package/core/server/api/endpoints/webhooks.js +2 -19
  89. package/core/server/data/exporter/table-lists.js +2 -0
  90. package/core/server/data/migrations/versions/5.10/2022-08-15-05-34-add-expiry-at-column-to-members-products.js +6 -0
  91. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-member-created-events-table.js +11 -0
  92. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-subscription-created-events-table.js +11 -0
  93. package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js +45 -0
  94. package/core/server/data/migrations/versions/5.11/2022-08-22-11-03-add-member-alert-settings-columns-to-users.js +21 -0
  95. package/core/server/data/migrations/versions/5.11/2022-08-23-13-41-backfill-members-created-events.js +32 -0
  96. package/core/server/data/migrations/versions/5.11/2022-08-23-13-59-fix-page-resource-type.js +22 -0
  97. package/core/server/data/schema/fixtures/fixtures.json +3 -0
  98. package/core/server/data/schema/schema.js +24 -2
  99. package/core/server/lib/image/cached-image-size-from-url.js +52 -28
  100. package/core/server/lib/image/gravatar.js +8 -7
  101. package/core/server/lib/image/image-size.js +60 -56
  102. package/core/server/lib/image/image-utils.js +5 -2
  103. package/core/server/lib/image/index.js +14 -1
  104. package/core/server/models/action.js +0 -10
  105. package/core/server/models/api-key.js +3 -18
  106. package/core/server/models/base/plugins/actions.js +55 -0
  107. package/core/server/models/integration.js +3 -0
  108. package/core/server/models/label.js +3 -18
  109. package/core/server/models/member-created-event.js +26 -0
  110. package/core/server/models/member.js +54 -4
  111. package/core/server/models/offer.js +3 -0
  112. package/core/server/models/post.js +25 -18
  113. package/core/server/models/product.js +3 -0
  114. package/core/server/models/settings.js +4 -0
  115. package/core/server/models/subscription-created-event.js +30 -0
  116. package/core/server/models/tag.js +3 -18
  117. package/core/server/models/user.js +7 -19
  118. package/core/server/models/webhook.js +3 -0
  119. package/core/server/services/auth/api-key/admin.js +0 -3
  120. package/core/server/services/auth/passwordreset.js +0 -3
  121. package/core/server/services/comments/emails.js +3 -3
  122. package/core/server/services/explore/service.js +8 -6
  123. package/core/server/services/member-attribution/index.js +52 -0
  124. package/core/server/services/members/api.js +3 -1
  125. package/core/server/services/members/jobs/clean-expired-comped.js +105 -0
  126. package/core/server/services/members/jobs/index.js +27 -0
  127. package/core/server/services/members/service.js +14 -8
  128. package/core/server/services/public-config/site.js +1 -0
  129. package/core/server/services/route-settings/default-settings-manager.js +19 -17
  130. package/core/server/services/settings/settings-service.js +1 -1
  131. package/core/server/services/webhooks/trigger.js +14 -5
  132. package/core/shared/config/defaults.json +8 -3
  133. package/core/shared/labs.js +5 -2
  134. package/package.json +84 -83
  135. package/yarn.lock +440 -615
  136. package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
  137. package/components/tryghost-api-framework-0.0.0.tgz +0 -0
  138. package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
  139. package/components/tryghost-constants-0.0.0.tgz +0 -0
  140. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  141. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  142. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  143. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  144. package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
  145. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  146. package/components/tryghost-extract-api-key-0.0.0.tgz +0 -0
  147. package/components/tryghost-html-to-plaintext-0.0.0.tgz +0 -0
  148. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  149. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  150. package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
  151. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  152. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  153. package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
  154. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  155. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  156. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  157. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  158. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  159. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  160. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  161. package/components/tryghost-minifier-0.0.0.tgz +0 -0
  162. package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
  163. package/components/tryghost-mw-cache-control-0.0.0.tgz +0 -0
  164. package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
  165. package/components/tryghost-mw-session-from-token-0.0.0.tgz +0 -0
  166. package/components/tryghost-mw-update-user-last-seen-0.0.0.tgz +0 -0
  167. package/components/tryghost-mw-vhost-0.0.0.tgz +0 -0
  168. package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
  169. package/components/tryghost-package-json-0.0.0.tgz +0 -0
  170. package/components/tryghost-security-0.0.0.tgz +0 -0
  171. package/components/tryghost-session-service-0.0.0.tgz +0 -0
  172. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  173. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  174. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  175. package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
  176. package/core/built/admin/assets/chunk.143.1c158e8ef19f10e5439c.js +0 -41
  177. package/core/built/admin/assets/ghost-dark-4080c8f100997d4b8947f5da0e7946a1.css +0 -1
  178. package/core/built/admin/assets/ghost-facfdf4a7d9759c5b681340805f21fd8.css +0 -1
@@ -13,6 +13,9 @@ Label = ghostBookshelf.Model.extend({
13
13
 
14
14
  tableName: 'labels',
15
15
 
16
+ actionsCollectCRUD: true,
17
+ actionsResourceType: 'label',
18
+
16
19
  emitChange: function emitChange(event, options) {
17
20
  const eventToTrigger = 'label' + '.' + event;
18
21
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
@@ -62,24 +65,6 @@ Label = ghostBookshelf.Model.extend({
62
65
  const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
63
66
 
64
67
  return attrs;
65
- },
66
-
67
- getAction(event, options) {
68
- const actor = this.getActor(options);
69
-
70
- // @NOTE: we ignore internal updates (`options.context.internal`) for now
71
- if (!actor) {
72
- return;
73
- }
74
-
75
- // @TODO: implement context
76
- return {
77
- event: event,
78
- resource_id: this.id || this.previous('id'),
79
- resource_type: 'label',
80
- actor_id: actor.id,
81
- actor_type: actor.type
82
- };
83
68
  }
84
69
  }, {
85
70
  orderDefaultOptions: function orderDefaultOptions() {
@@ -0,0 +1,26 @@
1
+ const errors = require('@tryghost/errors');
2
+ const ghostBookshelf = require('./base');
3
+
4
+ const MemberCreatedEvent = ghostBookshelf.Model.extend({
5
+ tableName: 'members_created_events',
6
+
7
+ member() {
8
+ return this.belongsTo('Member', 'member_id', 'id');
9
+ },
10
+
11
+ attribution() {
12
+ return this.belongsTo('Post', 'attribution_id', 'id');
13
+ }
14
+ }, {
15
+ async edit() {
16
+ throw new errors.IncorrectUsageError({message: 'Cannot edit MemberCreatedEvent'});
17
+ },
18
+
19
+ async destroy() {
20
+ throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberCreatedEvent'});
21
+ }
22
+ });
23
+
24
+ module.exports = {
25
+ MemberCreatedEvent: ghostBookshelf.model('MemberCreatedEvent', MemberCreatedEvent)
26
+ };
@@ -3,6 +3,7 @@ const uuid = require('uuid');
3
3
  const _ = require('lodash');
4
4
  const config = require('../../shared/config');
5
5
  const {gravatar} = require('../lib/image');
6
+ const labs = require('../../shared/labs');
6
7
 
7
8
  const Member = ghostBookshelf.Model.extend({
8
9
  tableName: 'members',
@@ -39,6 +40,12 @@ const Member = ghostBookshelf.Model.extend({
39
40
  }, {
40
41
  key: 'newsletters',
41
42
  replacement: 'newsletters.slug'
43
+ }, {
44
+ key: 'signup',
45
+ replacement: 'signups.attribution_id'
46
+ }, {
47
+ key: 'conversion',
48
+ replacement: 'conversions.attribution_id'
42
49
  }];
43
50
  },
44
51
 
@@ -73,6 +80,18 @@ const Member = ghostBookshelf.Model.extend({
73
80
  joinFrom: 'member_id',
74
81
  joinTo: 'customer_id',
75
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'
76
95
  }
77
96
  };
78
97
  },
@@ -102,12 +121,16 @@ const Member = ghostBookshelf.Model.extend({
102
121
 
103
122
  products() {
104
123
  return this.belongsToMany('Product', 'members_products', 'member_id', 'product_id')
105
- .withPivot('sort_order')
124
+ .withPivot('sort_order', 'expiry_at')
106
125
  .query('orderBy', 'sort_order', 'ASC')
107
126
  .query((qb) => {
108
127
  // avoids bookshelf adding a `DISTINCT` to the query
109
128
  // we know the result set will already be unique and DISTINCT hurts query performance
110
- qb.columns('products.*');
129
+ if (labs.isSet('compExpiring')) {
130
+ qb.columns('products.*', 'expiry_at');
131
+ } else {
132
+ qb.columns('products.*');
133
+ }
111
134
  });
112
135
  },
113
136
 
@@ -155,6 +178,21 @@ const Member = ghostBookshelf.Model.extend({
155
178
  return this.hasMany('EmailRecipient', 'member_id', 'id');
156
179
  },
157
180
 
181
+ async updateTierExpiry(products = [], options = {}) {
182
+ if (!labs.isSet('compExpiring')) {
183
+ return;
184
+ }
185
+ for (const product of products) {
186
+ if (product?.expiry_at) {
187
+ const expiry = new Date(product.expiry_at);
188
+ const queryOptions = _.extend({}, options, {
189
+ query: {where: {product_id: product.id}}
190
+ });
191
+ await this.products().updatePivot({expiry_at: expiry}, queryOptions);
192
+ }
193
+ }
194
+ },
195
+
158
196
  serialize(options) {
159
197
  const defaultSerializedObject = ghostBookshelf.Model.prototype.serialize.call(this, options);
160
198
 
@@ -344,7 +382,13 @@ const Member = ghostBookshelf.Model.extend({
344
382
  return this.add(data, Object.assign({transacting}, unfilteredOptions));
345
383
  });
346
384
  }
347
- return ghostBookshelf.Model.add.call(this, data, unfilteredOptions);
385
+
386
+ return ghostBookshelf.Model.add.call(this, data, unfilteredOptions).then(async (member) => {
387
+ if (data.products) {
388
+ await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
389
+ }
390
+ return member;
391
+ });
348
392
  },
349
393
 
350
394
  edit(data, unfilteredOptions = {}) {
@@ -353,7 +397,13 @@ const Member = ghostBookshelf.Model.extend({
353
397
  return this.edit(data, Object.assign({transacting}, unfilteredOptions));
354
398
  });
355
399
  }
356
- return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions);
400
+
401
+ return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions).then(async (member) => {
402
+ if (data.products) {
403
+ await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
404
+ }
405
+ return member;
406
+ });
357
407
  },
358
408
 
359
409
  destroy(unfilteredOptions = {}) {
@@ -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
  }
@@ -38,6 +38,10 @@ Post = ghostBookshelf.Model.extend({
38
38
 
39
39
  tableName: 'posts',
40
40
 
41
+ actionsCollectCRUD: true,
42
+ actionsResourceType: 'post',
43
+ actionsExtraContext: ['type'],
44
+
41
45
  /**
42
46
  * @NOTE
43
47
  *
@@ -962,24 +966,6 @@ Post = ghostBookshelf.Model.extend({
962
966
 
963
967
  delete options.status;
964
968
  return filter;
965
- },
966
-
967
- getAction(event, options) {
968
- const actor = this.getActor(options);
969
-
970
- // @NOTE: we ignore internal updates (`options.context.internal`) for now
971
- if (!actor) {
972
- return;
973
- }
974
-
975
- // @TODO: implement context
976
- return {
977
- event: event,
978
- resource_id: this.id || this.previous('id'),
979
- resource_type: 'post',
980
- actor_id: actor.id,
981
- actor_type: actor.type
982
- };
983
969
  }
984
970
  }, {
985
971
  allowedFormats: ['mobiledoc', 'html', 'plaintext'],
@@ -1262,6 +1248,27 @@ Post = ghostBookshelf.Model.extend({
1262
1248
  return Promise.reject(new errors.NoPermissionError({
1263
1249
  message: tpl(messages.notEnoughPermission)
1264
1250
  }));
1251
+ },
1252
+
1253
+ countRelations() {
1254
+ return {
1255
+ signups(modelOrCollection) {
1256
+ modelOrCollection.query('columns', 'posts.*', (qb) => {
1257
+ qb.count('members_created_events.id')
1258
+ .from('members_created_events')
1259
+ .whereRaw('posts.id = members_created_events.attribution_id')
1260
+ .as('count__signups');
1261
+ });
1262
+ },
1263
+ conversions(modelOrCollection) {
1264
+ modelOrCollection.query('columns', 'posts.*', (qb) => {
1265
+ qb.count('members_subscription_created_events.id')
1266
+ .from('members_subscription_created_events')
1267
+ .whereRaw('posts.id = members_subscription_created_events.attribution_id')
1268
+ .as('count__conversions');
1269
+ });
1270
+ }
1271
+ };
1265
1272
  }
1266
1273
  });
1267
1274
 
@@ -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);
@@ -0,0 +1,30 @@
1
+ const errors = require('@tryghost/errors');
2
+ const ghostBookshelf = require('./base');
3
+
4
+ const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
5
+ tableName: 'members_subscription_created_events',
6
+
7
+ member() {
8
+ return this.belongsTo('Member', 'member_id', 'id');
9
+ },
10
+
11
+ subscription() {
12
+ return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
13
+ },
14
+
15
+ attribution() {
16
+ return this.belongsTo('Post', 'attribution_id', 'id');
17
+ }
18
+ }, {
19
+ async edit() {
20
+ throw new errors.IncorrectUsageError({message: 'Cannot edit SubscriptionCreatedEvent'});
21
+ },
22
+
23
+ async destroy() {
24
+ throw new errors.IncorrectUsageError({message: 'Cannot destroy SubscriptionCreatedEvent'});
25
+ }
26
+ });
27
+
28
+ module.exports = {
29
+ SubscriptionCreatedEvent: ghostBookshelf.model('SubscriptionCreatedEvent', SubscriptionCreatedEvent)
30
+ };
@@ -14,6 +14,9 @@ Tag = ghostBookshelf.Model.extend({
14
14
 
15
15
  tableName: 'tags',
16
16
 
17
+ actionsCollectCRUD: true,
18
+ actionsResourceType: 'tag',
19
+
17
20
  defaults: function defaults() {
18
21
  return {
19
22
  visibility: 'public'
@@ -138,24 +141,6 @@ Tag = ghostBookshelf.Model.extend({
138
141
  return attrs;
139
142
  },
140
143
 
141
- getAction(event, options) {
142
- const actor = this.getActor(options);
143
-
144
- // @NOTE: we ignore internal updates (`options.context.internal`) for now
145
- if (!actor) {
146
- return;
147
- }
148
-
149
- // @TODO: implement context
150
- return {
151
- event: event,
152
- resource_id: this.id || this.previous('id'),
153
- resource_type: 'tag',
154
- actor_id: actor.id,
155
- actor_type: actor.type
156
- };
157
- },
158
-
159
144
  defaultColumnsToFetch() {
160
145
  return ['id'];
161
146
  }
@@ -57,12 +57,18 @@ User = ghostBookshelf.Model.extend({
57
57
 
58
58
  tableName: 'users',
59
59
 
60
+ actionsCollectCRUD: true,
61
+ actionsResourceType: 'user',
62
+
60
63
  defaults: function defaults() {
61
64
  return {
62
65
  password: security.identifier.uid(50),
63
66
  visibility: 'public',
64
67
  status: 'active',
65
- 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
66
72
  };
67
73
  },
68
74
 
@@ -363,24 +369,6 @@ User = ghostBookshelf.Model.extend({
363
369
  delete options.status;
364
370
 
365
371
  return filter;
366
- },
367
-
368
- getAction(event, options) {
369
- const actor = this.getActor(options);
370
-
371
- // @NOTE: we ignore internal updates (`options.context.internal`) for now
372
- if (!actor) {
373
- return;
374
- }
375
-
376
- // @TODO: implement context
377
- return {
378
- event: event,
379
- resource_id: this.id || this.previous('id'),
380
- resource_type: 'user',
381
- actor_id: actor.id,
382
- actor_type: actor.type
383
- };
384
372
  }
385
373
  }, {
386
374
  orderDefaultOptions: function orderDefaultOptions() {
@@ -7,6 +7,9 @@ let Webhooks;
7
7
  Webhook = ghostBookshelf.Model.extend({
8
8
  tableName: 'webhooks',
9
9
 
10
+ actionsCollectCRUD: true,
11
+ actionsResourceType: 'webhook',
12
+
10
13
  defaults() {
11
14
  return {
12
15
  api_version: `v${ghostVersion.safe}`,
@@ -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);
@@ -45,7 +45,7 @@ class CommentsServiceEmails {
45
45
  accentColor: this.settingsCache.get('accent_color'),
46
46
  fromEmail: this.notificationFromAddress,
47
47
  toEmail: to,
48
- staffUrl: `${this.urlUtils.getAdminUrl()}ghost/#/settings/staff/${author.get('slug')}`
48
+ staffUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${author.get('slug')}`)
49
49
  };
50
50
 
51
51
  const {html, text} = await this.renderEmailTemplate('new-comment', templateData);
@@ -132,7 +132,7 @@ class CommentsServiceEmails {
132
132
  commentHtml: comment.get('html'),
133
133
  commentText: htmlToPlaintext.comment(comment.get('html')),
134
134
  commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
135
-
135
+
136
136
  reporterName: reporter.name,
137
137
  reporterEmail: reporter.email,
138
138
  reporter: reporter.name ? `${reporter.name} (${reporter.email})` : reporter.email,
@@ -144,7 +144,7 @@ class CommentsServiceEmails {
144
144
  accentColor: this.settingsCache.get('accent_color'),
145
145
  fromEmail: this.notificationFromAddress,
146
146
  toEmail: to,
147
- staffUrl: `${this.urlUtils.getAdminUrl()}ghost/#/settings/staff/${owner.get('slug')}`
147
+ staffUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${owner.get('slug')}`)
148
148
  };
149
149
 
150
150
  const {html, text} = await this.renderEmailTemplate('report', templateData);
@@ -26,17 +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} = 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
- url
39
+ url,
40
+ accent_color: accentColor,
41
+ locale
40
42
  },
41
43
  stripe: {
42
44
  configured: this.StripeService.api.configured,
@@ -45,10 +47,10 @@ module.exports = class ExploreService {
45
47
  };
46
48
 
47
49
  const mostRecentlyPublishedPost = await this.PostsService.getMostRecentlyPublishedPost();
48
- exploreProperties.mostRecentlyPublishedAt = mostRecentlyPublishedPost?.get('published_at') || null;
50
+ exploreProperties.most_recently_published_at = mostRecentlyPublishedPost?.get('published_at') || null;
49
51
 
50
52
  const owner = await this.UserModel.findOne({role: 'Owner', status: 'all'});
51
- exploreProperties.ownerEmail = owner?.get('email') || null;
53
+ exploreProperties.owner_email = owner?.get('email') || null;
52
54
 
53
55
  return exploreProperties;
54
56
  }
@@ -0,0 +1,52 @@
1
+ const urlService = require('../url');
2
+ const labsService = require('../../../shared/labs');
3
+ const DomainEvents = require('@tryghost/domain-events');
4
+ const urlUtils = require('../../../shared/url-utils');
5
+
6
+ class MemberAttributionServiceWrapper {
7
+ init() {
8
+ if (this.eventHandler) {
9
+ // Prevent creating duplicate DomainEvents subscribers
10
+ return;
11
+ }
12
+
13
+ // Wire up all the dependencies
14
+ const {MemberAttributionService, UrlTranslator, AttributionBuilder, EventHandler} = require('@tryghost/member-attribution');
15
+ const models = require('../../models');
16
+
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
+ const attributionBuilder = new AttributionBuilder({urlTranslator});
28
+
29
+ // Expose the service
30
+ this.service = new MemberAttributionService({
31
+ models: {
32
+ MemberCreatedEvent: models.MemberCreatedEvent,
33
+ SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
34
+ },
35
+ 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,
46
+ labsService
47
+ });
48
+ this.eventHandler.subscribe();
49
+ }
50
+ }
51
+
52
+ module.exports = new MemberAttributionServiceWrapper();
@@ -14,6 +14,7 @@ const urlUtils = require('../../../shared/url-utils');
14
14
  const labsService = require('../../../shared/labs');
15
15
  const offersService = require('../offers');
16
16
  const newslettersService = require('../newsletters');
17
+ const memberAttributionService = require('../member-attribution');
17
18
 
18
19
  const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
19
20
 
@@ -195,7 +196,8 @@ function createApiInstance(config) {
195
196
  stripeAPIService: stripeService.api,
196
197
  offersAPI: offersService.api,
197
198
  labsService: labsService,
198
- newslettersService: newslettersService
199
+ newslettersService: newslettersService,
200
+ memberAttributionService: memberAttributionService.service
199
201
  });
200
202
 
201
203
  return membersApiInstance;
@@ -0,0 +1,105 @@
1
+ const {parentPort} = require('worker_threads');
2
+ const ObjectId = require('bson-objectid').default;
3
+ const {chunk: chunkArray} = require('lodash');
4
+ const debug = require('@tryghost/debug')('jobs:clean-expired-comped');
5
+ const moment = require('moment');
6
+
7
+ // recurring job to clean expired complimentary subscriptions
8
+
9
+ // Exit early when cancelled to prevent stalling shutdown. No cleanup needed when cancelling as everything is idempotent and will pick up
10
+ // where it left off on next run
11
+ function cancel() {
12
+ if (parentPort) {
13
+ parentPort.postMessage('Expired complimentary subscriptions cleanup cancelled before completion');
14
+ parentPort.postMessage('cancelled');
15
+ } else {
16
+ setTimeout(() => {
17
+ process.exit(0);
18
+ }, 1000);
19
+ }
20
+ }
21
+
22
+ if (parentPort) {
23
+ parentPort.once('message', (message) => {
24
+ if (message === 'cancel') {
25
+ return cancel();
26
+ }
27
+ });
28
+ }
29
+
30
+ (async () => {
31
+ const cleanupStartDate = new Date();
32
+ const db = require('../../../data/db');
33
+ debug(`Starting cleanup of expired comp subscriptions`);
34
+ const expiredCompedRows = await db.knex('members_products')
35
+ .where('expiry_at', '<', moment.utc().startOf('day').toISOString())
36
+ .select('*');
37
+
38
+ let deletedExpiredSubs = 0;
39
+ let updatedMembers = 0;
40
+
41
+ // Run cleanup for expired comp subscriptions
42
+ // Removes expired comped entries from members_products table
43
+ // Updates affected members status to free from comped
44
+ // Adds member status event for going from comped to free
45
+ if (expiredCompedRows?.length) {
46
+ const rowIds = expiredCompedRows.map(d => d.id);
47
+ const memberIds = expiredCompedRows.map(d => d.member_id);
48
+
49
+ // Delete all expired comped rows
50
+ deletedExpiredSubs = await db.knex('members_products')
51
+ .whereIn('id', rowIds)
52
+ .del();
53
+
54
+ // fetch all comped members to update
55
+ const membersToUpdate = await db.knex('members')
56
+ .whereIn('id', memberIds)
57
+ .andWhere('status', 'comped');
58
+
59
+ const updateMemberIds = membersToUpdate.map(d => d.id);
60
+
61
+ // Update all comped members to free
62
+ updatedMembers = await db.knex('members')
63
+ .whereIn('id', updateMemberIds)
64
+ .update({
65
+ status: 'free'
66
+ });
67
+
68
+ const statusEvents = membersToUpdate.map((member) => {
69
+ const now = db.knex.raw('CURRENT_TIMESTAMP');
70
+
71
+ return {
72
+ id: ObjectId().toHexString(),
73
+ member_id: member.id,
74
+ from_status: member.status,
75
+ to_status: 'free',
76
+ created_at: now
77
+ };
78
+ });
79
+
80
+ // SQLite >= 3.32.0 can support 32766 host parameters
81
+ // each row uses 5 variables so ⌊32766/5⌋ = 6553
82
+ const chunkSize = 6553;
83
+
84
+ const chunks = chunkArray(statusEvents, chunkSize);
85
+
86
+ // Adds status event for members going comped->free
87
+ for (const chunk of chunks) {
88
+ await db.knex('members_status_events').insert(chunk);
89
+ }
90
+ }
91
+
92
+ let cleanupEndDate = new Date();
93
+
94
+ debug(`Removed ${deletedExpiredSubs} expired subscriptions, updated ${updatedMembers} members in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
95
+
96
+ if (parentPort) {
97
+ parentPort.postMessage(`Removed ${deletedExpiredSubs} expired subscriptions, updated ${updatedMembers} members in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
98
+ parentPort.postMessage('done');
99
+ } else {
100
+ // give the logging pipes time finish writing before exit
101
+ setTimeout(() => {
102
+ process.exit(0);
103
+ }, 1000);
104
+ }
105
+ })();