ghost 5.81.1 → 5.82.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/components/{tryghost-adapter-cache-memory-ttl-5.81.1.tgz → tryghost-adapter-cache-memory-ttl-5.82.1.tgz} +0 -0
  2. package/components/{tryghost-adapter-cache-redis-5.81.1.tgz → tryghost-adapter-cache-redis-5.82.1.tgz} +0 -0
  3. package/components/{tryghost-adapter-manager-5.81.1.tgz → tryghost-adapter-manager-5.82.1.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.82.1.tgz +0 -0
  5. package/components/{tryghost-api-framework-5.81.1.tgz → tryghost-api-framework-5.82.1.tgz} +0 -0
  6. package/components/tryghost-api-version-compatibility-service-5.82.1.tgz +0 -0
  7. package/components/{tryghost-audience-feedback-5.81.1.tgz → tryghost-audience-feedback-5.82.1.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.82.1.tgz +0 -0
  9. package/components/{tryghost-bootstrap-socket-5.81.1.tgz → tryghost-bootstrap-socket-5.82.1.tgz} +0 -0
  10. package/components/tryghost-collections-5.82.1.tgz +0 -0
  11. package/components/{tryghost-constants-5.81.1.tgz → tryghost-constants-5.82.1.tgz} +0 -0
  12. package/components/{tryghost-custom-theme-settings-service-5.81.1.tgz → tryghost-custom-theme-settings-service-5.82.1.tgz} +0 -0
  13. package/components/{tryghost-data-generator-5.81.1.tgz → tryghost-data-generator-5.82.1.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.82.1.tgz +0 -0
  15. package/components/{tryghost-donations-5.81.1.tgz → tryghost-donations-5.82.1.tgz} +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.82.1.tgz +0 -0
  17. package/components/{tryghost-email-addresses-5.81.1.tgz → tryghost-email-addresses-5.82.1.tgz} +0 -0
  18. package/components/{tryghost-email-analytics-provider-mailgun-5.81.1.tgz → tryghost-email-analytics-provider-mailgun-5.82.1.tgz} +0 -0
  19. package/components/tryghost-email-analytics-service-5.82.1.tgz +0 -0
  20. package/components/{tryghost-email-content-generator-5.81.1.tgz → tryghost-email-content-generator-5.82.1.tgz} +0 -0
  21. package/components/{tryghost-email-events-5.81.1.tgz → tryghost-email-events-5.82.1.tgz} +0 -0
  22. package/components/{tryghost-email-service-5.81.1.tgz → tryghost-email-service-5.82.1.tgz} +0 -0
  23. package/components/tryghost-email-suppression-list-5.82.1.tgz +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.81.1.tgz → tryghost-express-dynamic-redirects-5.82.1.tgz} +0 -0
  25. package/components/tryghost-external-media-inliner-5.82.1.tgz +0 -0
  26. package/components/tryghost-extract-api-key-5.82.1.tgz +0 -0
  27. package/components/tryghost-ghost-5.82.1.tgz +0 -0
  28. package/components/{tryghost-html-to-plaintext-5.81.1.tgz → tryghost-html-to-plaintext-5.82.1.tgz} +0 -0
  29. package/components/tryghost-i18n-5.82.1.tgz +0 -0
  30. package/components/tryghost-importer-handler-content-files-5.82.1.tgz +0 -0
  31. package/components/{tryghost-importer-revue-5.81.1.tgz → tryghost-importer-revue-5.82.1.tgz} +0 -0
  32. package/components/{tryghost-in-memory-repository-5.81.1.tgz → tryghost-in-memory-repository-5.82.1.tgz} +0 -0
  33. package/components/{tryghost-job-manager-5.81.1.tgz → tryghost-job-manager-5.82.1.tgz} +0 -0
  34. package/components/{tryghost-link-redirects-5.81.1.tgz → tryghost-link-redirects-5.82.1.tgz} +0 -0
  35. package/components/{tryghost-link-replacer-5.81.1.tgz → tryghost-link-replacer-5.82.1.tgz} +0 -0
  36. package/components/{tryghost-link-tracking-5.81.1.tgz → tryghost-link-tracking-5.82.1.tgz} +0 -0
  37. package/components/{tryghost-magic-link-5.81.1.tgz → tryghost-magic-link-5.82.1.tgz} +0 -0
  38. package/components/{tryghost-mail-events-5.81.1.tgz → tryghost-mail-events-5.82.1.tgz} +0 -0
  39. package/components/tryghost-mailgun-client-5.82.1.tgz +0 -0
  40. package/components/{tryghost-member-attribution-5.81.1.tgz → tryghost-member-attribution-5.82.1.tgz} +0 -0
  41. package/components/{tryghost-member-events-5.81.1.tgz → tryghost-member-events-5.82.1.tgz} +0 -0
  42. package/components/tryghost-members-api-5.82.1.tgz +0 -0
  43. package/components/tryghost-members-csv-5.82.1.tgz +0 -0
  44. package/components/{tryghost-members-events-service-5.81.1.tgz → tryghost-members-events-service-5.82.1.tgz} +0 -0
  45. package/components/{tryghost-members-importer-5.81.1.tgz → tryghost-members-importer-5.82.1.tgz} +0 -0
  46. package/components/{tryghost-members-offers-5.81.1.tgz → tryghost-members-offers-5.82.1.tgz} +0 -0
  47. package/components/tryghost-members-payments-5.82.1.tgz +0 -0
  48. package/components/tryghost-members-ssr-5.82.1.tgz +0 -0
  49. package/components/tryghost-members-stripe-service-5.82.1.tgz +0 -0
  50. package/components/{tryghost-mentions-email-report-5.81.1.tgz → tryghost-mentions-email-report-5.82.1.tgz} +0 -0
  51. package/components/tryghost-milestones-5.82.1.tgz +0 -0
  52. package/components/{tryghost-minifier-5.81.1.tgz → tryghost-minifier-5.82.1.tgz} +0 -0
  53. package/components/{tryghost-model-to-domain-event-interceptor-5.81.1.tgz → tryghost-model-to-domain-event-interceptor-5.82.1.tgz} +0 -0
  54. package/components/tryghost-mw-api-version-mismatch-5.82.1.tgz +0 -0
  55. package/components/{tryghost-mw-cache-control-5.81.1.tgz → tryghost-mw-cache-control-5.82.1.tgz} +0 -0
  56. package/components/{tryghost-mw-error-handler-5.81.1.tgz → tryghost-mw-error-handler-5.82.1.tgz} +0 -0
  57. package/components/tryghost-mw-session-from-token-5.82.1.tgz +0 -0
  58. package/components/tryghost-mw-update-user-last-seen-5.82.1.tgz +0 -0
  59. package/components/{tryghost-mw-version-match-5.81.1.tgz → tryghost-mw-version-match-5.82.1.tgz} +0 -0
  60. package/components/tryghost-mw-vhost-5.82.1.tgz +0 -0
  61. package/components/{tryghost-nql-filter-expansions-5.81.1.tgz → tryghost-nql-filter-expansions-5.82.1.tgz} +0 -0
  62. package/components/{tryghost-oembed-service-5.81.1.tgz → tryghost-oembed-service-5.82.1.tgz} +0 -0
  63. package/components/{tryghost-package-json-5.81.1.tgz → tryghost-package-json-5.82.1.tgz} +0 -0
  64. package/components/{tryghost-post-events-5.81.1.tgz → tryghost-post-events-5.82.1.tgz} +0 -0
  65. package/components/tryghost-post-revisions-5.82.1.tgz +0 -0
  66. package/components/{tryghost-posts-service-5.81.1.tgz → tryghost-posts-service-5.82.1.tgz} +0 -0
  67. package/components/tryghost-recommendations-5.82.1.tgz +0 -0
  68. package/components/tryghost-referrers-5.82.1.tgz +0 -0
  69. package/components/{tryghost-security-5.81.1.tgz → tryghost-security-5.82.1.tgz} +0 -0
  70. package/components/tryghost-session-service-5.82.1.tgz +0 -0
  71. package/components/tryghost-settings-path-manager-5.82.1.tgz +0 -0
  72. package/components/tryghost-slack-notifications-5.82.1.tgz +0 -0
  73. package/components/{tryghost-staff-service-5.81.1.tgz → tryghost-staff-service-5.82.1.tgz} +0 -0
  74. package/components/{tryghost-stats-service-5.81.1.tgz → tryghost-stats-service-5.82.1.tgz} +0 -0
  75. package/components/{tryghost-tiers-5.81.1.tgz → tryghost-tiers-5.82.1.tgz} +0 -0
  76. package/components/{tryghost-update-check-service-5.81.1.tgz → tryghost-update-check-service-5.82.1.tgz} +0 -0
  77. package/components/tryghost-verification-trigger-5.82.1.tgz +0 -0
  78. package/components/tryghost-version-notifications-data-service-5.82.1.tgz +0 -0
  79. package/components/tryghost-webmentions-5.82.1.tgz +0 -0
  80. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  81. package/core/built/admin/assets/admin-x-demo/{index-c81d9844.mjs → index-d851aa59.mjs} +3 -3
  82. package/core/built/admin/assets/admin-x-demo/{modals-b9180276.mjs → modals-e82d1de3.mjs} +2 -2
  83. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-eef74c9c.mjs → CodeEditorView-2c507362.mjs} +2 -2
  84. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  85. package/core/built/admin/assets/admin-x-settings/{index-5f812a27.mjs → index-6e1dd00b.mjs} +2 -2
  86. package/core/built/admin/assets/admin-x-settings/{index-e986b71a.mjs → index-e75a9553.mjs} +1202 -1191
  87. package/core/built/admin/assets/admin-x-settings/{modals-24778772.mjs → modals-2a4dc4c5.mjs} +926 -916
  88. package/core/built/admin/assets/{chunk.524.a8bf57854398790b354d.js → chunk.524.326ea641bd5fcb846f80.js} +6 -6
  89. package/core/built/admin/assets/{chunk.582.6fc384a7dc0eb0bcdd3a.js → chunk.582.93b7d492a9b57a70a3cf.js} +4 -4
  90. package/core/built/admin/assets/{chunk.682.597be201dde4abdd4e68.js → chunk.682.d7ae35ab7054f9997ce6.js} +13 -11
  91. package/core/built/admin/assets/{ghost-dark-6385e71bb2c2fe3231b529c0e4eabbe6.css → ghost-dark-7488b47ac1849513fde7b7c8e51b3a70.css} +1 -1
  92. package/core/built/admin/assets/{ghost-c3ba0161decd868a3a79d6f4264c3286.js → ghost-de0cb83d4074a8874313e5f81872406b.js} +200 -195
  93. package/core/built/admin/assets/{ghost-85d66247f3eb43a3d462b89c9280e805.css → ghost-f84b3f229da06680df6c13ebd46ab671.css} +1 -1
  94. package/core/built/admin/index.html +5 -5
  95. package/core/frontend/services/routing/RouterManager.js +5 -0
  96. package/core/frontend/services/routing/SubscribeRouter.js +27 -0
  97. package/core/frontend/services/routing/controllers/index.js +4 -0
  98. package/core/frontend/services/routing/controllers/subscribe.js +35 -0
  99. package/core/frontend/views/subscribe.hbs +36 -0
  100. package/core/server/data/migrations/versions/5.82/2024-03-25-16-46-10-add-email-recipients-email-id-indexes.js +17 -0
  101. package/core/server/data/migrations/versions/5.82/2024-03-25-16-51-29-drop-email-recipients-non-email-id-indexes.js +17 -0
  102. package/core/server/data/schema/schema.js +7 -4
  103. package/core/server/services/email-analytics/lib/queries.js +15 -8
  104. package/core/server/services/members/MembersConfigProvider.js +3 -1
  105. package/core/server/services/stripe/service.js +2 -0
  106. package/core/server/web/members/app.js +3 -0
  107. package/core/shared/labs.js +4 -2
  108. package/package.json +149 -149
  109. package/yarn.lock +160 -160
  110. package/components/tryghost-announcement-bar-settings-5.81.1.tgz +0 -0
  111. package/components/tryghost-api-version-compatibility-service-5.81.1.tgz +0 -0
  112. package/components/tryghost-bookshelf-repository-5.81.1.tgz +0 -0
  113. package/components/tryghost-collections-5.81.1.tgz +0 -0
  114. package/components/tryghost-domain-events-5.81.1.tgz +0 -0
  115. package/components/tryghost-dynamic-routing-events-5.81.1.tgz +0 -0
  116. package/components/tryghost-email-analytics-service-5.81.1.tgz +0 -0
  117. package/components/tryghost-email-suppression-list-5.81.1.tgz +0 -0
  118. package/components/tryghost-external-media-inliner-5.81.1.tgz +0 -0
  119. package/components/tryghost-extract-api-key-5.81.1.tgz +0 -0
  120. package/components/tryghost-ghost-5.81.1.tgz +0 -0
  121. package/components/tryghost-i18n-5.81.1.tgz +0 -0
  122. package/components/tryghost-importer-handler-content-files-5.81.1.tgz +0 -0
  123. package/components/tryghost-mailgun-client-5.81.1.tgz +0 -0
  124. package/components/tryghost-members-api-5.81.1.tgz +0 -0
  125. package/components/tryghost-members-csv-5.81.1.tgz +0 -0
  126. package/components/tryghost-members-payments-5.81.1.tgz +0 -0
  127. package/components/tryghost-members-ssr-5.81.1.tgz +0 -0
  128. package/components/tryghost-members-stripe-service-5.81.1.tgz +0 -0
  129. package/components/tryghost-milestones-5.81.1.tgz +0 -0
  130. package/components/tryghost-mw-api-version-mismatch-5.81.1.tgz +0 -0
  131. package/components/tryghost-mw-session-from-token-5.81.1.tgz +0 -0
  132. package/components/tryghost-mw-update-user-last-seen-5.81.1.tgz +0 -0
  133. package/components/tryghost-mw-vhost-5.81.1.tgz +0 -0
  134. package/components/tryghost-post-revisions-5.81.1.tgz +0 -0
  135. package/components/tryghost-recommendations-5.81.1.tgz +0 -0
  136. package/components/tryghost-referrers-5.81.1.tgz +0 -0
  137. package/components/tryghost-session-service-5.81.1.tgz +0 -0
  138. package/components/tryghost-settings-path-manager-5.81.1.tgz +0 -0
  139. package/components/tryghost-slack-notifications-5.81.1.tgz +0 -0
  140. package/components/tryghost-verification-trigger-5.81.1.tgz +0 -0
  141. package/components/tryghost-version-notifications-data-service-5.81.1.tgz +0 -0
  142. package/components/tryghost-webmentions-5.81.1.tgz +0 -0
  143. /package/core/built/admin/assets/{chunk.682.597be201dde4abdd4e68.js.LICENSE.txt → chunk.682.d7ae35ab7054f9997ce6.js.LICENSE.txt} +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%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%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.81%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%22%3Atrue%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%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22a4b7e8c0f1%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%2276d88862ac%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%225793443e3b%22%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%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.82%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%22%3Atrue%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%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22a4b7e8c0f1%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%22c95f5407a8%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22d20f997405%22%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-0ede59da8efb5e28fa929557f7ff7154.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-85d66247f3eb43a3d462b89c9280e805.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-f84b3f229da06680df6c13ebd46ab671.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -57,8 +57,8 @@
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
59
  <script src="assets/vendor-f9bad0b8c0149699f84c0bd9d325068e.js"></script>
60
- <script src="assets/chunk.682.597be201dde4abdd4e68.js"></script>
61
- <script src="assets/chunk.524.a8bf57854398790b354d.js"></script>
62
- <script src="assets/ghost-c3ba0161decd868a3a79d6f4264c3286.js"></script>
60
+ <script src="assets/chunk.682.d7ae35ab7054f9997ce6.js"></script>
61
+ <script src="assets/chunk.524.326ea641bd5fcb846f80.js"></script>
62
+ <script src="assets/ghost-de0cb83d4074a8874313e5f81872406b.js"></script>
63
63
  </body>
64
64
  </html>
@@ -8,6 +8,7 @@ const PreviewRouter = require('./PreviewRouter');
8
8
  const ParentRouter = require('./ParentRouter');
9
9
  const EmailRouter = require('./EmailRouter');
10
10
  const UnsubscribeRouter = require('./UnsubscribeRouter');
11
+ const SubscribeRouter = require('./SubscribeRouter');
11
12
 
12
13
  // This emits its own routing events
13
14
  const events = require('../../../server/lib/common/events');
@@ -109,6 +110,10 @@ class RouterManager {
109
110
  this.siteRouter.mountRouter(unsubscribeRouter.router());
110
111
  this.registry.setRouter('unsubscribeRouter', unsubscribeRouter);
111
112
 
113
+ const subscribeRouter = new SubscribeRouter();
114
+ this.siteRouter.mountRouter(subscribeRouter.router());
115
+ this.registry.setRouter('subscribeRouter', subscribeRouter);
116
+
112
117
  if (RESOURCE_CONFIG.QUERY.email) {
113
118
  const emailRouter = new EmailRouter(RESOURCE_CONFIG);
114
119
  this.siteRouter.mountRouter(emailRouter.router());
@@ -0,0 +1,27 @@
1
+ const ParentRouter = require('./ParentRouter');
2
+ const controllers = require('./controllers');
3
+
4
+ /**
5
+ * @description Subscribe Router.
6
+ *
7
+ * "/subscribe/" -> Subscribe Router
8
+ */
9
+ class SubscribeRouter extends ParentRouter {
10
+ constructor() {
11
+ super('SubscribeRouter');
12
+
13
+ // @NOTE: hardcoded, not configurable
14
+ this.route = {value: '/confirm_signup/'};
15
+ this._registerRoutes();
16
+ }
17
+
18
+ /**
19
+ * @description Register all routes of this router.
20
+ * @private
21
+ */
22
+ _registerRoutes() {
23
+ this.mountRoute(this.route.value, controllers.subscribe);
24
+ }
25
+ }
26
+
27
+ module.exports = SubscribeRouter;
@@ -29,5 +29,9 @@ module.exports = {
29
29
 
30
30
  get unsubscribe() {
31
31
  return require('./unsubscribe');
32
+ },
33
+
34
+ get subscribe() {
35
+ return require('./subscribe');
32
36
  }
33
37
  };
@@ -0,0 +1,35 @@
1
+ const debug = require('@tryghost/debug')('services:routing:controllers:subscribe');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const handlebars = require('handlebars');
5
+ const assetHelper = require('../../../helpers/asset');
6
+ const {settingsCache} = require('../../../services/proxy');
7
+
8
+ handlebars.registerHelper('asset', assetHelper);
9
+
10
+ module.exports = async function subscribeController(req, res) {
11
+ debug('subscribeController');
12
+
13
+ // Get the query params
14
+ const {query} = req;
15
+ const token = query.token || null;
16
+ const action = query.action || null;
17
+ const ref = query.r || null;
18
+
19
+ if (!token || !action) {
20
+ return res.send(404);
21
+ }
22
+
23
+ // Prepare context for rendering template
24
+ const context = {
25
+ token,
26
+ action,
27
+ r: ref,
28
+ meta_title: settingsCache.get('title'),
29
+ accent_color: settingsCache.get('accent_color')
30
+ };
31
+ // Compile and render the template
32
+ const rawTemplate = fs.readFileSync(path.resolve(path.join(__dirname, '../../../views/subscribe.hbs'))).toString();
33
+ const template = handlebars.compile(rawTemplate);
34
+ return res.send(template(context));
35
+ };
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
+ <title>{{title}}</title>
8
+ <link rel="stylesheet" href="{{asset "public/ghost.css" hasMinFile="true"}}" />
9
+ </head>
10
+ <body>
11
+ <div class="gh-app">
12
+ <div class="gh-viewport">
13
+ <main class="gh-main" role="main" id="main">
14
+ <div class="gh-flow">
15
+ <div class="gh-flow-content-wrap">
16
+ <h1>Subscribing to {{meta_title}}</h1>
17
+ <p>If you are not redirected automatically, please click the "Subscribe" button below.</p>
18
+ <form id="gh-subscribe-form" action="/members/api/member" method="POST">
19
+ <input type="hidden" name="token" value="{{token}}" />
20
+ <input type="hidden" name="action" value="{{action}}" />
21
+ <input type="hidden" name="r" value="{{r}}" />
22
+ <button class="gh-btn" style="background-color: {{accent_color}}; color: #FFFFFF" type="submit"><span>Subscribe</span></button>
23
+ </form>
24
+ </div>
25
+ </div>
26
+ </main>
27
+ </div>
28
+ </div>
29
+ <script>
30
+ const main = document.getElementById('main');
31
+ main.style.display = 'none';
32
+ const form = document.getElementById('gh-subscribe-form');
33
+ form.submit();
34
+ </script>
35
+ </body>
36
+ </html>
@@ -0,0 +1,17 @@
1
+ // For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
2
+
3
+ const {createNonTransactionalMigration} = require('../../utils');
4
+ const commands = require('../../../schema/commands');
5
+
6
+ module.exports = createNonTransactionalMigration(
7
+ async function up(knex) {
8
+ await commands.addIndex('email_recipients', ['email_id', 'delivered_at'], knex);
9
+ await commands.addIndex('email_recipients', ['email_id', 'opened_at'], knex);
10
+ await commands.addIndex('email_recipients', ['email_id', 'failed_at'], knex);
11
+ },
12
+ async function down(knex) {
13
+ await commands.dropIndex('email_recipients', ['email_id', 'delivered_at'], knex);
14
+ await commands.dropIndex('email_recipients', ['email_id', 'opened_at'], knex);
15
+ await commands.dropIndex('email_recipients', ['email_id', 'failed_at'], knex);
16
+ }
17
+ );
@@ -0,0 +1,17 @@
1
+ // For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
2
+
3
+ const {createNonTransactionalMigration} = require('../../utils');
4
+ const commands = require('../../../schema/commands');
5
+
6
+ module.exports = createNonTransactionalMigration(
7
+ async function up(knex) {
8
+ await commands.dropIndex('email_recipients', ['delivered_at'], knex);
9
+ await commands.dropIndex('email_recipients', ['opened_at'], knex);
10
+ await commands.dropIndex('email_recipients', ['failed_at'], knex);
11
+ },
12
+ async function down(knex) {
13
+ await commands.addIndex('email_recipients', ['delivered_at'], knex);
14
+ await commands.addIndex('email_recipients', ['opened_at'], knex);
15
+ await commands.addIndex('email_recipients', ['failed_at'], knex);
16
+ }
17
+ );
@@ -865,14 +865,17 @@ module.exports = {
865
865
  member_id: {type: 'string', maxlength: 24, nullable: false, index: true},
866
866
  batch_id: {type: 'string', maxlength: 24, nullable: false, references: 'email_batches.id'},
867
867
  processed_at: {type: 'dateTime', nullable: true},
868
- delivered_at: {type: 'dateTime', nullable: true, index: true},
869
- opened_at: {type: 'dateTime', nullable: true, index: true},
870
- failed_at: {type: 'dateTime', nullable: true, index: true},
868
+ delivered_at: {type: 'dateTime', nullable: true},
869
+ opened_at: {type: 'dateTime', nullable: true},
870
+ failed_at: {type: 'dateTime', nullable: true},
871
871
  member_uuid: {type: 'string', maxlength: 36, nullable: false},
872
872
  member_email: {type: 'string', maxlength: 191, nullable: false},
873
873
  member_name: {type: 'string', maxlength: 191, nullable: true},
874
874
  '@@INDEXES@@': [
875
- ['email_id', 'member_email']
875
+ ['email_id', 'member_email'],
876
+ ['email_id', 'delivered_at'],
877
+ ['email_id', 'opened_at'],
878
+ ['email_id', 'failed_at']
876
879
  ]
877
880
  },
878
881
  email_recipient_failures: {
@@ -41,10 +41,16 @@ module.exports = {
41
41
  },
42
42
 
43
43
  async aggregateEmailStats(emailId) {
44
+ const {totalCount} = await db.knex('emails').select(db.knex.raw('email_count as totalCount')).where('id', emailId).first() || {totalCount: 0};
45
+ // use IS NULL here because that will typically match far fewer rows than IS NOT NULL making the query faster
46
+ const [undeliveredCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND delivered_at IS NULL', [emailId]);
47
+ const [openedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND opened_at IS NOT NULL', [emailId]);
48
+ const [failedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND failed_at IS NOT NULL', [emailId]);
49
+
44
50
  await db.knex('emails').update({
45
- delivered_count: db.knex.raw(`(SELECT COUNT(id) FROM email_recipients WHERE email_id = ? AND delivered_at IS NOT NULL)`, [emailId]),
46
- opened_count: db.knex.raw(`(SELECT COUNT(id) FROM email_recipients WHERE email_id = ? AND opened_at IS NOT NULL)`, [emailId]),
47
- failed_count: db.knex.raw(`(SELECT COUNT(id) FROM email_recipients WHERE email_id = ? AND failed_at IS NOT NULL)`, [emailId])
51
+ delivered_count: totalCount - undeliveredCount.count,
52
+ opened_count: openedCount.count,
53
+ failed_count: failedCount.count
48
54
  }).where('id', emailId);
49
55
  },
50
56
 
@@ -56,15 +62,16 @@ module.exports = {
56
62
  .where('emails.track_opens', true)
57
63
  .first() || {};
58
64
 
65
+ const [emailCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ?', [memberId]);
66
+ const [emailOpenedCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ? AND opened_at IS NOT NULL', [memberId]);
67
+
59
68
  const updateQuery = {
60
- email_count: db.knex.raw('(SELECT COUNT(id) FROM email_recipients WHERE member_id = ?)', [memberId]),
61
- email_opened_count: db.knex.raw('(SELECT COUNT(id) FROM email_recipients WHERE member_id = ? AND opened_at IS NOT NULL)', [memberId])
69
+ email_count: emailCount.count,
70
+ email_opened_count: emailOpenedCount.count
62
71
  };
63
72
 
64
73
  if (trackedEmailCount >= MIN_EMAIL_COUNT_FOR_OPEN_RATE) {
65
- updateQuery.email_open_rate = db.knex.raw(`
66
- ROUND(((SELECT COUNT(id) FROM email_recipients WHERE member_id = ? AND opened_at IS NOT NULL) * 1.0 / ? * 100), 0)
67
- `, [memberId, trackedEmailCount]);
74
+ updateQuery.email_open_rate = Math.round(emailOpenedCount.count / trackedEmailCount * 100);
68
75
  }
69
76
 
70
77
  await db.knex('members')
@@ -2,6 +2,7 @@ const logging = require('@tryghost/logging');
2
2
  const {URL} = require('url');
3
3
  const crypto = require('crypto');
4
4
  const createKeypair = require('keypair');
5
+ const labs = require('../../../shared/labs');
5
6
 
6
7
  class MembersConfigProvider {
7
8
  /**
@@ -87,7 +88,8 @@ class MembersConfigProvider {
87
88
  }
88
89
 
89
90
  getSigninURL(token, type, referrer) {
90
- const siteUrl = this._urlUtils.urlFor({relativeUrl: '/members/'}, true);
91
+ const relativeUrl = ['signup', 'subscribe'].includes(type) && labs.isSet('membersSpamPrevention') ? '/confirm_signup/' : '/members/';
92
+ const siteUrl = this._urlUtils.urlFor({relativeUrl}, true);
91
93
  const signinURL = new URL(siteUrl);
92
94
  signinURL.searchParams.set('token', token);
93
95
  signinURL.searchParams.set('action', type);
@@ -11,6 +11,7 @@ const {getConfig} = require('./config');
11
11
  const settingsHelpers = require('../settings-helpers');
12
12
  const donationService = require('../donations');
13
13
  const staffService = require('../staff');
14
+ const labs = require('../../../shared/labs');
14
15
 
15
16
  async function configureApi() {
16
17
  const cfg = getConfig({settingsHelpers, config, urlUtils});
@@ -30,6 +31,7 @@ const debouncedConfigureApi = _.debounce(() => {
30
31
  }, 600);
31
32
 
32
33
  module.exports = new StripeService({
34
+ labs,
33
35
  membersService,
34
36
  models: _.pick(models, [
35
37
  'Product',
@@ -37,6 +37,9 @@ module.exports = function setupMembersApp() {
37
37
  // Initializes members specific routes as well as assigns members specific data to the req/res objects
38
38
  // We don't want to add global bodyParser middleware as that interferes with stripe webhook requests on - `/webhooks`.
39
39
 
40
+ // Double opt-in subscription handling
41
+ membersApp.post('/api/member', membersService.api.middleware.createMemberFromToken);
42
+
40
43
  // Manage newsletter subscription via unsubscribe link
41
44
  membersApp.get('/api/member/newsletters', middleware.getMemberNewsletters);
42
45
  membersApp.put('/api/member/newsletters', bodyParser.json({limit: '50mb'}), middleware.updateMemberNewsletters);
@@ -24,12 +24,14 @@ const GA_FEATURES = [
24
24
  'listUnsubscribeHeader',
25
25
  'filterEmailDisabled',
26
26
  'newEmailAddresses',
27
- 'portalImprovements'
27
+ 'portalImprovements',
28
+ 'onboardingChecklist'
28
29
  ];
29
30
 
30
31
  // NOTE: this allowlist is meant to be used to filter out any unexpected
31
32
  // input for the "labs" setting value
32
33
  const BETA_FEATURES = [
34
+ 'additionalPaymentMethods',
33
35
  'i18n',
34
36
  'activitypub',
35
37
  'webmentions'
@@ -49,7 +51,7 @@ const ALPHA_FEATURES = [
49
51
  'lexicalIndicators',
50
52
  // 'adminXOffers',
51
53
  'adminXDemo',
52
- 'onboardingChecklist'
54
+ 'membersSpamPrevention'
53
55
  ];
54
56
 
55
57
  module.exports.GA_KEYS = [...GA_FEATURES];