ghost 5.11.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 (102) hide show
  1. package/components/{tryghost-adapter-manager-5.11.0.tgz → 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-5.11.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.11.0.tgz → tryghost-email-analytics-service-5.12.0.tgz} +0 -0
  10. package/components/{tryghost-email-content-generator-5.11.0.tgz → 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-5.12.0.tgz +0 -0
  14. package/components/{tryghost-job-manager-5.11.0.tgz → 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.11.0.tgz → tryghost-mailgun-client-5.12.0.tgz} +0 -0
  17. package/components/{tryghost-member-analytics-service-5.11.0.tgz → 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-5.11.0.tgz → tryghost-members-csv-5.12.0.tgz} +0 -0
  23. package/components/{tryghost-members-events-service-5.11.0.tgz → tryghost-members-events-service-5.12.0.tgz} +0 -0
  24. package/components/{tryghost-members-importer-5.11.0.tgz → 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.11.0.tgz → tryghost-members-stripe-service-5.12.0.tgz} +0 -0
  29. package/components/{tryghost-minifier-5.11.0.tgz → tryghost-minifier-5.12.0.tgz} +0 -0
  30. package/components/{tryghost-mw-api-version-mismatch-5.11.0.tgz → 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.11.0.tgz → tryghost-mw-error-handler-5.12.0.tgz} +0 -0
  33. package/components/{tryghost-mw-session-from-token-5.11.0.tgz → tryghost-mw-session-from-token-5.12.0.tgz} +0 -0
  34. package/components/{tryghost-mw-update-user-last-seen-5.11.0.tgz → 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.11.0.tgz → 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.11.0.tgz → tryghost-session-service-5.12.0.tgz} +0 -0
  40. package/components/{tryghost-settings-path-manager-5.11.0.tgz → 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.11.0.tgz → 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.11.0.tgz → 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.14589cc066b8120b73e3.js → chunk.143.da85cf08f47cfe520bf6.js} +5 -5
  47. package/core/built/admin/assets/{chunk.178.131e85a10d2031148425.js → chunk.178.29d3fb3dc6811b673a00.js} +4 -4
  48. package/core/built/admin/assets/{ghost-40f5bd12d121c54bbc39e7939e78244f.js → ghost-0526c96b20843697927c1d06a9010197.js} +144 -154
  49. package/core/built/admin/assets/{ghost-dark-7b2825a050b0382630180f48aa78ea5d.css → ghost-dark-e4d6987343ee26af534a06c1b9639d97.css} +1 -1
  50. package/core/built/admin/assets/{ghost-1b0d7c731511bb738ec457d2932c43c0.css → ghost-f62b0e78ddcd947273873bdeba4abc3c.css} +1 -1
  51. package/core/built/admin/assets/icons/event-canceled-subscription--feature-attribution.svg +6 -0
  52. package/core/built/admin/assets/icons/event-comment--feature-attribution.svg +3 -0
  53. package/core/built/admin/assets/icons/event-email-delivery-failed--feature-attribution.svg +6 -0
  54. package/core/built/admin/assets/icons/event-logged-in--feature-attribution.svg +5 -0
  55. package/core/built/admin/assets/icons/event-made-a-payment--feature-attribution.svg +7 -0
  56. package/core/built/admin/assets/icons/event-opened-email--feature-attribution.svg +6 -0
  57. package/core/built/admin/assets/icons/event-received-email--feature-attribution.svg +5 -0
  58. package/core/built/admin/assets/icons/event-signed-up--feature-attribution.svg +6 -0
  59. package/core/built/admin/assets/icons/event-started-subscription--feature-attribution.svg +6 -0
  60. package/core/built/admin/assets/icons/event-subscribed-to-email--feature-attribution.svg +8 -0
  61. package/core/built/admin/assets/icons/event-subscriptions--feature-attribution.svg +5 -0
  62. package/core/built/admin/assets/icons/event-unsubscribed-from-email--feature-attribution.svg +5 -0
  63. package/core/built/admin/assets/{vendor-741dc0e4078e044a0c9bfaad104de8b3.js → vendor-52613f40d62355e9ac64cbfa211169bb.js} +79 -55
  64. package/core/built/admin/index.html +5 -5
  65. package/core/frontend/helpers/search.js +5 -20
  66. package/core/frontend/meta/get-meta.js +1 -2
  67. package/core/frontend/meta/image-dimensions.js +47 -39
  68. package/core/server/data/schema/schema.js +20 -4
  69. package/core/server/models/action.js +6 -9
  70. package/core/server/models/member-created-event.js +10 -2
  71. package/core/server/models/member-paid-subscription-event.js +4 -0
  72. package/core/server/models/subscription-created-event.js +10 -2
  73. package/core/server/models/user.js +37 -6
  74. package/core/server/services/member-attribution/index.js +2 -2
  75. package/core/server/services/members/api.js +4 -0
  76. package/core/server/services/members/service.js +6 -3
  77. package/core/server/services/staff/index.js +26 -0
  78. package/core/shared/labs.js +6 -5
  79. package/package.json +83 -81
  80. package/yarn.lock +13 -18
  81. package/components/tryghost-api-framework-5.11.0.tgz +0 -0
  82. package/components/tryghost-bootstrap-socket-5.11.0.tgz +0 -0
  83. package/components/tryghost-constants-5.11.0.tgz +0 -0
  84. package/components/tryghost-custom-theme-settings-service-5.11.0.tgz +0 -0
  85. package/components/tryghost-domain-events-5.11.0.tgz +0 -0
  86. package/components/tryghost-email-analytics-provider-mailgun-5.11.0.tgz +0 -0
  87. package/components/tryghost-express-dynamic-redirects-5.11.0.tgz +0 -0
  88. package/components/tryghost-extract-api-key-5.11.0.tgz +0 -0
  89. package/components/tryghost-html-to-plaintext-5.11.0.tgz +0 -0
  90. package/components/tryghost-magic-link-5.11.0.tgz +0 -0
  91. package/components/tryghost-member-attribution-5.11.0.tgz +0 -0
  92. package/components/tryghost-member-events-5.11.0.tgz +0 -0
  93. package/components/tryghost-members-analytics-ingress-5.11.0.tgz +0 -0
  94. package/components/tryghost-members-api-5.11.0.tgz +0 -0
  95. package/components/tryghost-members-offers-5.11.0.tgz +0 -0
  96. package/components/tryghost-members-payments-5.11.0.tgz +0 -0
  97. package/components/tryghost-members-ssr-5.11.0.tgz +0 -0
  98. package/components/tryghost-mw-cache-control-5.11.0.tgz +0 -0
  99. package/components/tryghost-mw-vhost-5.11.0.tgz +0 -0
  100. package/components/tryghost-oembed-service-5.11.0.tgz +0 -0
  101. package/components/tryghost-security-5.11.0.tgz +0 -0
  102. package/components/tryghost-verification-trigger-5.11.0.tgz +0 -0
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.11%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.12%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-bc9d2c9e5c8a33f0c92e81189d48e04c.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-1b0d7c731511bb738ec457d2932c43c0.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-f62b0e78ddcd947273873bdeba4abc3c.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -53,9 +53,9 @@
53
53
 
54
54
  <div id="ember-basic-dropdown-wormhole"></div>
55
55
 
56
- <script src="assets/vendor-741dc0e4078e044a0c9bfaad104de8b3.js"></script>
56
+ <script src="assets/vendor-52613f40d62355e9ac64cbfa211169bb.js"></script>
57
57
  <script src="assets/chunk.579.65e09dd89eec70d059a0.js"></script>
58
- <script src="assets/chunk.143.14589cc066b8120b73e3.js"></script>
59
- <script src="assets/ghost-40f5bd12d121c54bbc39e7939e78244f.js"></script>
58
+ <script src="assets/chunk.143.da85cf08f47cfe520bf6.js"></script>
59
+ <script src="assets/ghost-0526c96b20843697927c1d06a9010197.js"></script>
60
60
  </body>
61
61
  </html>
@@ -4,26 +4,11 @@ const {SafeString} = require('../services/handlebars');
4
4
  const {labs} = require('../services/proxy');
5
5
 
6
6
  function search() {
7
- const svg = `<style>.gh-search-icon {
8
- display: inline-flex;
9
- justify-content: center;
10
- align-items: center;
11
- width: 32px;
12
- height: 32px;
13
- padding: 0;
14
- border: 0;
15
- color: inherit;
16
- background-color: transparent;
17
- cursor: pointer;
18
- outline: none;
19
- }</style>
20
- <button class="gh-search-icon" aria-label="search" data-ghost-search>
21
- <svg width="20" height="20" fill="none" viewBox="0 0 24 24">
22
- <path d="M14.949 14.949a1 1 0 0 1 1.414 0l6.344 6.344a1 1 0 0 1-1.414 1.414l-6.344-6.344a1 1 0 0 1 0-1.414Z"
23
- fill="currentColor"/>
24
- <path d="M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm-9 7a9 9 0 1 1 18 0 9 9 0 0 1-18 0Z" fill="currentColor"/>
25
- </svg>
26
- </button>`;
7
+ // We want this to output as one line, but splitting for readability
8
+ const svg = '<button class="gh-search-icon" aria-label="search" data-ghost-search '
9
+ + 'style="display: inline-flex; justify-content: center; align-items: center; width: 32px; height: 32px; padding: 0; border: 0; color: inherit; background-color: transparent; cursor: pointer; outline: none;">'
10
+ + '<svg width="20" height="20" fill="none" viewBox="0 0 24 24"><path d="M14.949 14.949a1 1 0 0 1 1.414 0l6.344 6.344a1 1 0 0 1-1.414 1.414l-6.344-6.344a1 1 0 0 1 0-1.414Z" fill="currentColor"/>'
11
+ + '<path d="M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm-9 7a9 9 0 1 1 18 0 9 9 0 0 1-18 0Z" fill="currentColor"/></svg></button>';
27
12
 
28
13
  return new SafeString(svg);
29
14
  }
@@ -1,4 +1,3 @@
1
- const Promise = require('bluebird');
2
1
  const settingsCache = require('../../shared/settings-cache');
3
2
  const urlUtils = require('../../shared/url-utils');
4
3
  const logging = require('@tryghost/logging');
@@ -81,7 +80,7 @@ function getMetaData(data, root) {
81
80
  }
82
81
 
83
82
  // @TODO: wrap this in a utility function
84
- return Promise.props(getImageDimensions(metaData)).then(function () {
83
+ return getImageDimensions(metaData).then(function () {
85
84
  metaData.structuredData = getStructuredData(metaData);
86
85
  metaData.schema = getSchema(metaData, data);
87
86
 
@@ -1,15 +1,14 @@
1
- const Promise = require('bluebird');
2
1
  const _ = require('lodash');
3
2
  const imageSizeCache = require('../../server/lib/image').cachedImageSizeFromUrl;
4
3
 
5
4
  /**
6
5
  * Get Image dimensions
7
6
  * @param {object} metaData
8
- * @returns {object} metaData
7
+ * @returns {Promise<object>} metaData
9
8
  * @description for image properties in meta data (coverImage, authorImage and site.logo), `getCachedImageSizeFromUrl` is
10
9
  * called to receive image width and height
11
10
  */
12
- function getImageDimensions(metaData) {
11
+ async function getImageDimensions(metaData) {
13
12
  const fetch = {
14
13
  coverImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.coverImage.url),
15
14
  authorImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.authorImage.url),
@@ -17,45 +16,54 @@ function getImageDimensions(metaData) {
17
16
  logo: imageSizeCache.getCachedImageSizeFromUrl(metaData.site.logo.url)
18
17
  };
19
18
 
20
- return Promise
21
- .props(fetch)
22
- .then(function (imageObj) {
23
- _.forEach(imageObj, function (key, value) {
24
- if (_.has(key, 'width') && _.has(key, 'height')) {
25
- // We have some restrictions for publisher.logo:
26
- // The image needs to be <=600px wide and <=60px high (ideally exactly 600px x 60px).
27
- // Unless we have proper image-handling (see https://github.com/TryGhost/Ghost/issues/4453),
28
- // we will fake it in some cases or not produce an imageObject at all.
29
- if (value === 'logo') {
30
- if (key.height <= 60 && key.width <= 600) {
31
- _.assign(metaData.site[value], {
32
- dimensions: {
33
- width: key.width,
34
- height: key.height
35
- }
36
- });
37
- } else if (key.width === key.height) {
38
- // CASE: the logo is too large, but it is a square. We fake it...
39
- _.assign(metaData.site[value], {
40
- dimensions: {
41
- width: 60,
42
- height: 60
43
- }
44
- });
19
+ const [coverImage, authorImage, ogImage, logo] = await Promise.all([
20
+ fetch.coverImage,
21
+ fetch.authorImage,
22
+ fetch.ogImage,
23
+ fetch.logo
24
+ ]);
25
+ const imageObj = {
26
+ coverImage,
27
+ authorImage,
28
+ ogImage,
29
+ logo
30
+ };
31
+
32
+ _.forEach(imageObj, function (key, value) {
33
+ if (_.has(key, 'width') && _.has(key, 'height')) {
34
+ // We have some restrictions for publisher.logo:
35
+ // The image needs to be <=600px wide and <=60px high (ideally exactly 600px x 60px).
36
+ // Unless we have proper image-handling (see https://github.com/TryGhost/Ghost/issues/4453),
37
+ // we will fake it in some cases or not produce an imageObject at all.
38
+ if (value === 'logo') {
39
+ if (key.height <= 60 && key.width <= 600) {
40
+ _.assign(metaData.site[value], {
41
+ dimensions: {
42
+ width: key.width,
43
+ height: key.height
45
44
  }
46
- } else {
47
- _.assign(metaData[value], {
48
- dimensions: {
49
- width: key.width,
50
- height: key.height
51
- }
52
- });
53
- }
45
+ });
46
+ } else if (key.width === key.height) {
47
+ // CASE: the logo is too large, but it is a square. We fake it...
48
+ _.assign(metaData.site[value], {
49
+ dimensions: {
50
+ width: 60,
51
+ height: 60
52
+ }
53
+ });
54
54
  }
55
- });
55
+ } else {
56
+ _.assign(metaData[value], {
57
+ dimensions: {
58
+ width: key.width,
59
+ height: key.height
60
+ }
61
+ });
62
+ }
63
+ }
64
+ });
56
65
 
57
- return metaData;
58
- });
66
+ return metaData;
59
67
  }
60
68
 
61
69
  module.exports = getImageDimensions;
@@ -483,9 +483,17 @@ module.exports = {
483
483
  created_at: {type: 'dateTime', nullable: false},
484
484
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
485
485
  attribution_id: {type: 'string', maxlength: 24, nullable: true},
486
- 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
+ },
487
491
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
488
- 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
+ }
489
497
  },
490
498
  members_cancel_events: {
491
499
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -613,7 +621,11 @@ module.exports = {
613
621
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
614
622
  subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
615
623
  attribution_id: {type: 'string', maxlength: 24, nullable: true},
616
- 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
+ },
617
629
  attribution_url: {type: 'string', maxlength: 2000, nullable: true}
618
630
  },
619
631
  offer_redemptions: {
@@ -628,7 +640,11 @@ module.exports = {
628
640
  member_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'members.id', cascadeDelete: true},
629
641
  subscribed: {type: 'bool', nullable: false, defaultTo: true},
630
642
  created_at: {type: 'dateTime', nullable: false},
631
- 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
+ },
632
648
  newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id', cascadeDelete: false}
633
649
  },
634
650
  stripe_products: {
@@ -1,24 +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);
18
+ return this.morphTo('resource', ['resource_type', 'resource_id'], ...this.candidates());
22
19
  }
23
20
  }, {
24
21
  orderDefaultOptions: function orderDefaultOptions() {
@@ -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) {
@@ -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() {
@@ -488,6 +488,33 @@ User = ghostBookshelf.Model.extend({
488
488
  return query.fetch(options);
489
489
  },
490
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
+
491
518
  /**
492
519
  * ### Edit
493
520
  *
@@ -1005,10 +1032,10 @@ User = ghostBookshelf.Model.extend({
1005
1032
  let ownerRole;
1006
1033
  let contextUser;
1007
1034
 
1008
- return Promise.join(
1035
+ return Promise.all([
1009
1036
  ghostBookshelf.model('Role').findOne({name: 'Owner'}),
1010
1037
  User.findOne({id: options.context.user}, {withRelated: ['roles']})
1011
- )
1038
+ ])
1012
1039
  .then((results) => {
1013
1040
  ownerRole = results[0];
1014
1041
  contextUser = results[1];
@@ -1021,8 +1048,10 @@ User = ghostBookshelf.Model.extend({
1021
1048
  }));
1022
1049
  }
1023
1050
 
1024
- return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Administrator'}),
1025
- 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
+ ]);
1026
1055
  })
1027
1056
  .then((results) => {
1028
1057
  const adminRole = results[0];
@@ -1049,9 +1078,11 @@ User = ghostBookshelf.Model.extend({
1049
1078
  }
1050
1079
 
1051
1080
  // convert owner to admin
1052
- return Promise.join(contextUser.roles().updatePivot({role_id: adminRole.id}),
1081
+ return Promise.all([
1082
+ contextUser.roles().updatePivot({role_id: adminRole.id}),
1053
1083
  user.roles().updatePivot({role_id: ownerRole.id}),
1054
- user.id);
1084
+ user.id
1085
+ ]);
1055
1086
  })
1056
1087
  .then((results) => {
1057
1088
  return Users.forge()
@@ -24,7 +24,7 @@ class MemberAttributionServiceWrapper {
24
24
  }
25
25
  });
26
26
 
27
- const attributionBuilder = new AttributionBuilder({urlTranslator});
27
+ this.attributionBuilder = new AttributionBuilder({urlTranslator});
28
28
 
29
29
  // Expose the service
30
30
  this.service = new MemberAttributionService({
@@ -32,7 +32,7 @@ class MemberAttributionServiceWrapper {
32
32
  MemberCreatedEvent: models.MemberCreatedEvent,
33
33
  SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
34
34
  },
35
- attributionBuilder,
35
+ attributionBuilder: this.attributionBuilder,
36
36
  labsService
37
37
  });
38
38
 
@@ -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');
@@ -0,0 +1,26 @@
1
+ class StaffServiceWrapper {
2
+ init() {
3
+ const StaffService = require('@tryghost/staff-service');
4
+
5
+ const config = require('../../../shared/config');
6
+ const logging = require('@tryghost/logging');
7
+ const models = require('../../models');
8
+ const {GhostMailer} = require('../mail');
9
+ const mailer = new GhostMailer();
10
+ const settingsCache = require('../../../shared/settings-cache');
11
+ const urlService = require('../url');
12
+ const urlUtils = require('../../../shared/url-utils');
13
+
14
+ this.api = new StaffService({
15
+ config,
16
+ logging,
17
+ models,
18
+ mailer,
19
+ settingsCache,
20
+ urlService,
21
+ urlUtils
22
+ });
23
+ }
24
+ }
25
+
26
+ module.exports = new StaffServiceWrapper();
@@ -17,21 +17,22 @@ const messages = {
17
17
  const GA_FEATURES = [
18
18
  'newsletterPaywall',
19
19
  'freeTrial',
20
- 'compExpiring'
20
+ 'compExpiring',
21
+ 'searchHelper',
22
+ 'emailAlerts'
21
23
  ];
22
24
 
23
25
  // NOTE: this allowlist is meant to be used to filter out any unexpected
24
26
  // input for the "labs" setting value
25
27
  const BETA_FEATURES = [
26
- 'activitypub'
28
+ 'activitypub',
29
+ 'memberAttribution'
27
30
  ];
28
31
 
29
32
  const ALPHA_FEATURES = [
30
33
  'auditLog',
31
34
  'urlCache',
32
- 'beforeAfterCard',
33
- 'memberAttribution',
34
- 'searchHelper'
35
+ 'beforeAfterCard'
35
36
  ];
36
37
 
37
38
  module.exports.GA_KEYS = [...GA_FEATURES];