ghost 4.43.1 → 4.46.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 (120) hide show
  1. package/Gruntfile.js +1 -1
  2. package/core/boot.js +2 -0
  3. package/core/built/assets/{chunk.3.6e2ed2d00856e12bd81a.js → chunk.3.52b444495dfcf50afb0b.js} +20 -20
  4. package/core/built/assets/ghost-dark-155e039c0d991b7af75dea8cd3846b11.css +1 -0
  5. package/core/built/assets/{ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js → ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js} +845 -670
  6. package/core/built/assets/ghost.min-bd8cd0185fd5dfc8291502f801e443e6.css +1 -0
  7. package/core/built/assets/icons/clock.svg +1 -1
  8. package/core/built/assets/icons/email-at.svg +1 -0
  9. package/core/built/assets/icons/email-body.svg +1 -0
  10. package/core/built/assets/icons/email-footer.svg +1 -0
  11. package/core/built/assets/icons/email-header.svg +1 -0
  12. package/core/built/assets/icons/email-member.svg +1 -0
  13. package/core/built/assets/icons/email-name.svg +1 -0
  14. package/core/built/assets/icons/member.svg +1 -3
  15. package/core/built/assets/icons/send-email.svg +1 -1
  16. package/core/built/assets/img/abstract-2-2937e2902b64360d0cbe4cec8bd8479b.jpg +0 -0
  17. package/core/built/assets/img/abstract-c52b2f4208e7fd2e7b8abd8b1eec4f7b.jpg +0 -0
  18. package/core/built/assets/img/community-background-3f501ff1d764d0cb81f7c2cbacfc6503.jpg +0 -0
  19. package/core/built/assets/img/community-be8c1dcecfb157f2bfba5cababc8e686.jpg +0 -0
  20. package/core/built/assets/img/newsletter-1-197ae8063dfb2e22278d355198029c9e.jpg +0 -0
  21. package/core/built/assets/img/newsletter-2-5a2c7693ea9380d4282061302c01267a.jpg +0 -0
  22. package/core/built/assets/img/resource-1-722f202795856e4a5596c8a3b7bedc43.jpg +0 -0
  23. package/core/built/assets/{vendor.min-21f79c68a284acb1b70039f3f63e5507.js → vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js} +868 -523
  24. package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -3
  25. package/core/frontend/apps/amp/lib/views/amp.hbs +5 -3
  26. package/core/frontend/helpers/get.js +1 -1
  27. package/core/frontend/services/routing/controllers/unsubscribe.js +22 -0
  28. package/core/frontend/web/middleware/cors.js +56 -0
  29. package/core/frontend/web/middleware/index.js +1 -0
  30. package/core/frontend/web/middleware/static-theme.js +8 -8
  31. package/core/frontend/web/site.js +1 -48
  32. package/core/server/api/canary/authentication.js +2 -2
  33. package/core/server/api/canary/members.js +3 -0
  34. package/core/server/api/canary/newsletters.js +86 -4
  35. package/core/server/api/canary/posts.js +1 -0
  36. package/core/server/api/canary/stats.js +11 -2
  37. package/core/server/api/canary/utils/serializers/input/members.js +22 -0
  38. package/core/server/api/canary/utils/serializers/output/mappers/pages.js +1 -0
  39. package/core/server/api/canary/utils/serializers/output/mappers/posts.js +2 -0
  40. package/core/server/api/canary/utils/serializers/output/members.js +13 -2
  41. package/core/server/api/shared/http.js +1 -1
  42. package/core/server/api/v2/utils/serializers/output/utils/mapper.js +2 -0
  43. package/core/server/api/v3/utils/serializers/output/utils/mapper.js +3 -0
  44. package/core/server/data/importer/importers/data/settings.js +0 -3
  45. package/core/server/data/migrations/utils.js +40 -0
  46. package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +5 -5
  47. package/core/server/data/migrations/versions/4.44/2022-04-06-15-22-populate-type-column-for-paid-subscription-events.js +21 -0
  48. package/core/server/data/migrations/versions/4.44/2022-04-08-11-54-add-cancelled-events.js +51 -0
  49. package/core/server/data/migrations/versions/4.44/2022-04-11-08-24-add-newsletter-permissions.js +33 -0
  50. package/core/server/data/migrations/versions/4.44/2022-04-11-10-54-add-mrr-to-subscriptions.js +8 -0
  51. package/core/server/data/migrations/versions/4.44/2022-04-12-07-33-fill-mrr.js +29 -0
  52. package/core/server/data/migrations/versions/4.44/2022-04-13-12-00-remove-newsletter-sender-name-not-null-constraint.js +33 -0
  53. package/core/server/data/migrations/versions/4.44/2022-04-15-07-53-add-offer-id-to-subscriptions.js +9 -0
  54. package/core/server/data/migrations/versions/4.45/2022-04-19-12-23-backfill-subscriptions-offers.js +60 -0
  55. package/core/server/data/migrations/versions/4.45/2022-04-20-11-25-add-newsletter-read-permission.js +9 -0
  56. package/core/server/data/migrations/versions/4.45/2022-04-21-02-55-add-notifications-key-entry-to-settings-table.js +8 -0
  57. package/core/server/data/migrations/versions/4.46/2022-04-13-12-00-add-created-at-newsletters.js +6 -0
  58. package/core/server/data/migrations/versions/4.46/2022-04-13-12-01-add-updated-at-newsletters.js +6 -0
  59. package/core/server/data/migrations/versions/4.46/2022-04-13-12-02-fill-created-at-newsletters.js +19 -0
  60. package/core/server/data/migrations/versions/4.46/2022-04-13-12-03-drop-nullable-created-at-newsletters.js +3 -0
  61. package/core/server/data/migrations/versions/4.46/2022-04-13-12-08-newsletters-show-header-name.js +7 -0
  62. package/core/server/data/migrations/versions/4.46/2022-04-13-12-57-add-uuid-column-to-newsletters.js +8 -0
  63. package/core/server/data/migrations/versions/4.46/2022-04-13-12-58-fill-uuid-for-newsletters.js +19 -0
  64. package/core/server/data/migrations/versions/4.46/2022-04-13-12-59-drop-nullable-uuid-newsletters.js +3 -0
  65. package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +85 -0
  66. package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +66 -0
  67. package/core/server/data/migrations/versions/4.46/2022-04-22-07-43-add-newsletter-id-to-subscribe-events.js +9 -0
  68. package/core/server/data/migrations/versions/4.46/2022-04-27-07-59-set-newsletter-id-subscribe-events.js +31 -0
  69. package/core/server/data/schema/commands.js +14 -0
  70. package/core/server/data/schema/default-settings/default-settings.json +4 -0
  71. package/core/server/data/schema/fixtures/fixtures.json +32 -1
  72. package/core/server/data/schema/schema.js +15 -8
  73. package/core/server/models/base/plugins/generate-slug.js +2 -2
  74. package/core/server/models/email.js +4 -0
  75. package/core/server/models/label.js +1 -1
  76. package/core/server/models/member-subscribe-event.js +4 -0
  77. package/core/server/models/member.js +29 -0
  78. package/core/server/models/newsletter.js +101 -11
  79. package/core/server/models/post.js +15 -5
  80. package/core/server/models/role.js +1 -1
  81. package/core/server/models/stripe-customer-subscription.js +4 -0
  82. package/core/server/models/tag.js +1 -1
  83. package/core/server/models/user.js +1 -1
  84. package/core/server/services/api-version-compatibility/index.js +29 -0
  85. package/core/server/services/auth/members/index.js +1 -1
  86. package/core/server/services/auth/setup.js +17 -7
  87. package/core/server/services/mega/email-preview.js +4 -1
  88. package/core/server/services/mega/mega.js +86 -27
  89. package/core/server/services/mega/post-email-serializer.js +17 -14
  90. package/core/server/services/mega/template.js +24 -3
  91. package/core/server/services/members/api.js +2 -2
  92. package/core/server/services/members/middleware.js +69 -2
  93. package/core/server/services/members/service.js +7 -12
  94. package/core/server/services/newsletters/emails/verify-email.js +166 -0
  95. package/core/server/services/newsletters/index.js +14 -7
  96. package/core/server/services/newsletters/service.js +237 -6
  97. package/core/server/services/posts/posts-service.js +18 -1
  98. package/core/server/services/stats/service.js +2 -6
  99. package/core/server/services/users.js +20 -20
  100. package/core/server/web/admin/views/default-prod.html +4 -4
  101. package/core/server/web/admin/views/default.html +4 -4
  102. package/core/server/web/api/app.js +3 -0
  103. package/core/server/web/api/canary/admin/app.js +3 -0
  104. package/core/server/web/api/canary/admin/routes.js +3 -0
  105. package/core/server/web/api/canary/content/app.js +3 -0
  106. package/core/server/web/api/middleware/cors.js +1 -1
  107. package/core/server/web/api/v2/admin/app.js +3 -0
  108. package/core/server/web/api/v2/content/app.js +3 -0
  109. package/core/server/web/api/v3/admin/app.js +3 -0
  110. package/core/server/web/api/v3/content/app.js +3 -0
  111. package/core/server/web/members/app.js +5 -0
  112. package/core/shared/config/defaults.json +2 -2
  113. package/core/shared/labs.js +4 -2
  114. package/core/shared/settings-cache/public.js +1 -1
  115. package/package.json +82 -78
  116. package/yarn.lock +1062 -679
  117. package/core/built/assets/ghost-dark-1933079797e24ccb8839657020830be5.css +0 -1
  118. package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +0 -1
  119. package/core/server/services/stats/lib/members-stats-service.js +0 -161
  120. package/core/server/services/stats/lib/mrr-stats-service.js +0 -154
@@ -0,0 +1,166 @@
1
+ module.exports = ({email, url}) => `
2
+ <!doctype html>
3
+ <html>
4
+ <head>
5
+ <meta name="viewport" content="width=device-width">
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
+ <title>Confirm your email address</title>
8
+ <style>
9
+ /* -------------------------------------
10
+ RESPONSIVE AND MOBILE FRIENDLY STYLES
11
+ ------------------------------------- */
12
+ @media only screen and (max-width: 620px) {
13
+ table[class=body] h1 {
14
+ font-size: 28px !important;
15
+ margin-bottom: 10px !important;
16
+ }
17
+ table[class=body] p,
18
+ table[class=body] ul,
19
+ table[class=body] ol,
20
+ table[class=body] td,
21
+ table[class=body] span,
22
+ table[class=body] a {
23
+ font-size: 16px !important;
24
+ }
25
+ table[class=body] .wrapper,
26
+ table[class=body] .article {
27
+ padding: 10px !important;
28
+ }
29
+ table[class=body] .content {
30
+ padding: 0 !important;
31
+ }
32
+ table[class=body] .container {
33
+ padding: 0 !important;
34
+ width: 100% !important;
35
+ }
36
+ table[class=body] .main {
37
+ border-left-width: 0 !important;
38
+ border-radius: 0 !important;
39
+ border-right-width: 0 !important;
40
+ }
41
+ table[class=body] .btn table {
42
+ width: 100% !important;
43
+ }
44
+ table[class=body] .btn a {
45
+ width: 100% !important;
46
+ }
47
+ table[class=body] .img-responsive {
48
+ height: auto !important;
49
+ max-width: 100% !important;
50
+ width: auto !important;
51
+ }
52
+ }
53
+ /* -------------------------------------
54
+ PRESERVE THESE STYLES IN THE HEAD
55
+ ------------------------------------- */
56
+ @media all {
57
+ .ExternalClass {
58
+ width: 100%;
59
+ }
60
+ .ExternalClass,
61
+ .ExternalClass p,
62
+ .ExternalClass span,
63
+ .ExternalClass font,
64
+ .ExternalClass td,
65
+ .ExternalClass div {
66
+ line-height: 100%;
67
+ }
68
+ .recipient-link a {
69
+ color: inherit !important;
70
+ font-family: inherit !important;
71
+ font-size: inherit !important;
72
+ font-weight: inherit !important;
73
+ line-height: inherit !important;
74
+ text-decoration: none !important;
75
+ }
76
+ #MessageViewBody a {
77
+ color: inherit;
78
+ text-decoration: none;
79
+ font-size: inherit;
80
+ font-family: inherit;
81
+ font-weight: inherit;
82
+ line-height: inherit;
83
+ }
84
+ }
85
+ hr {
86
+ border-width: 0;
87
+ height: 0;
88
+ margin-top: 34px;
89
+ margin-bottom: 34px;
90
+ border-bottom-width: 1px;
91
+ border-bottom-color: #EEF5F8;
92
+ }
93
+ </style>
94
+ </head>
95
+ <body style="background-color: #F4F8FB; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
96
+ <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #F4F8FB;">
97
+ <tr>
98
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
99
+ <td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 600px; padding: 10px; width: 600px;">
100
+ <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
101
+
102
+ <!-- START CENTERED WHITE CONTAINER -->
103
+ <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
104
+
105
+ <!-- START MAIN CONTENT AREA -->
106
+ <tr>
107
+ <td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 40px 50px;">
108
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
109
+ <tr>
110
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
111
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
112
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Please confirm your email address with this link:</p>
113
+ <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
114
+ <tbody>
115
+ <tr>
116
+ <td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-bottom: 35px;">
117
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
118
+ <tbody>
119
+ <tr>
120
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: #15212A; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #15212A; border: solid 1px #15212A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: #15212A;" data-test-verify-link>Confirm email address</a> </td>
121
+ </tr>
122
+ </tbody>
123
+ </table>
124
+ </td>
125
+ </tr>
126
+ </tbody>
127
+ </table>
128
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
129
+ <hr/>
130
+ <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
131
+ <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; line-height: 21px; margin-top: 0; color: #738A94;">${url}</p>
132
+ </td>
133
+ </tr>
134
+ </table>
135
+ </td>
136
+ </tr>
137
+
138
+ <!-- END MAIN CONTENT AREA -->
139
+ </table>
140
+
141
+ <!-- START FOOTER -->
142
+ <div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
143
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
144
+ <tr>
145
+ <td class="content-block" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-bottom: 5px; padding-top: 15px; font-size: 13px; line-height: 21px; color: #738A94; text-align: center;">
146
+ If you did not make this request, you can simply delete this message.<br/>This email address will not be used.
147
+ </td>
148
+ </tr>
149
+ <tr>
150
+ <td class="content-block" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 13px; color: #738A94; text-align: center;">
151
+ <span class="recipient-link" style="color: #738A94; font-size: 13px; text-align: center;">Sent to <a href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 13px; text-align: center;">${email}</a></span>
152
+ </td>
153
+ </tr>
154
+ </table>
155
+ </div>
156
+ <!-- END FOOTER -->
157
+
158
+ <!-- END CENTERED WHITE CONTAINER -->
159
+ </div>
160
+ </td>
161
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
162
+ </tr>
163
+ </table>
164
+ </body>
165
+ </html>
166
+ `;
@@ -1,10 +1,17 @@
1
1
  const NewslettersService = require('./service.js');
2
+ const SingleUseTokenProvider = require('../members/SingleUseTokenProvider');
3
+ const mail = require('../mail');
4
+ const models = require('../../models');
5
+ const urlUtils = require('../../../shared/url-utils');
6
+ const limitService = require('../limits');
2
7
 
3
- /**
4
- * @returns {NewslettersService} instance of the NewslettersService
5
- */
6
- const getNewslettersServiceInstance = ({NewsletterModel}) => {
7
- return new NewslettersService({NewsletterModel});
8
- };
8
+ const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
9
9
 
10
- module.exports = getNewslettersServiceInstance;
10
+ module.exports = new NewslettersService({
11
+ NewsletterModel: models.Newsletter,
12
+ MemberModel: models.Member,
13
+ mail,
14
+ singleUseTokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY),
15
+ urlUtils,
16
+ limitService
17
+ });
@@ -1,24 +1,255 @@
1
+ const _ = require('lodash');
2
+ const MagicLink = require('@tryghost/magic-link');
3
+ const logging = require('@tryghost/logging');
4
+ const verifyEmailTemplate = require('./emails/verify-email');
5
+ const debug = require('@tryghost/debug')('services:newsletters');
6
+
1
7
  class NewslettersService {
2
8
  /**
3
9
  *
4
10
  * @param {Object} options
5
11
  * @param {Object} options.NewsletterModel
12
+ * @param {Object} options.MemberModel
13
+ * @param {Object} options.mail
14
+ * @param {Object} options.singleUseTokenProvider
15
+ * @param {Object} options.urlUtils
16
+ * @param {ILimitService} options.limitService
6
17
  */
7
- constructor({NewsletterModel}) {
18
+ constructor({NewsletterModel, MemberModel, mail, singleUseTokenProvider, urlUtils, limitService}) {
8
19
  this.NewsletterModel = NewsletterModel;
20
+ this.MemberModel = MemberModel;
21
+ this.urlUtils = urlUtils;
22
+ /** @private */
23
+ this.limitService = limitService;
24
+
25
+ /* email verification setup */
26
+
27
+ this.ghostMailer = new mail.GhostMailer();
28
+
29
+ const {transporter, getSubject, getText, getHTML, getSigninURL} = {
30
+ transporter: {
31
+ sendMail() {
32
+ // noop - overridden in `sendEmailVerificationMagicLink`
33
+ }
34
+ },
35
+ getSubject() {
36
+ // not used - overridden in `sendEmailVerificationMagicLink`
37
+ return `Verify email address`;
38
+ },
39
+ getText(url, type, email) {
40
+ return `
41
+ Hey there,
42
+
43
+ Please confirm your email address with this link:
44
+
45
+ ${url}
46
+
47
+ For your security, the link will expire in 24 hours time.
48
+
49
+ ---
50
+
51
+ Sent to ${email}
52
+ If you did not make this request, you can simply delete this message. This email address will not be used.
53
+ `;
54
+ },
55
+ getHTML(url, type, email) {
56
+ return verifyEmailTemplate({url, email});
57
+ },
58
+ getSigninURL(token) {
59
+ const adminUrl = urlUtils.urlFor('admin', true);
60
+ const signinURL = new URL(adminUrl);
61
+ signinURL.hash = `/settings/newsletters/?verifyEmail=${token}`;
62
+ return signinURL.href;
63
+ }
64
+ };
65
+
66
+ this.magicLinkService = new MagicLink({
67
+ transporter,
68
+ tokenProvider: singleUseTokenProvider,
69
+ getSigninURL,
70
+ getText,
71
+ getHTML,
72
+ getSubject
73
+ });
9
74
  }
10
75
 
11
76
  /**
12
- *
13
- * @param {Object} options browse options
14
- * @returns
77
+ * @public
78
+ * @param {Object} [options] options
79
+ * @returns {Promise<object>} JSONified Newsletter models
15
80
  */
16
- async browse(options) {
81
+ async browse(options = {}) {
17
82
  let newsletters = await this.NewsletterModel.findAll(options);
18
83
 
19
84
  return newsletters.toJSON();
20
85
  }
86
+ /**
87
+ * @public
88
+ * @param {object} attrs model properties
89
+ * @param {Object} [options] options
90
+ * @param {Object} [options] options.transacting
91
+ * @returns {Promise<{object}>} Newsetter Model with verification metadata
92
+ */
93
+ async add(attrs, options = {}) {
94
+ // create newsletter and assign members in the same transaction
95
+ if (options.opt_in_existing && !options.transacting) {
96
+ return this.NewsletterModel.transaction((transacting) => {
97
+ options.transacting = transacting;
98
+ return this.add(attrs, options);
99
+ });
100
+ }
101
+
102
+ await this.limitService.errorIfWouldGoOverLimit('newsletters');
103
+
104
+ // remove any email properties that are not allowed to be set without verification
105
+ const {cleanedAttrs, emailsToVerify} = await this.prepAttrsForEmailVerification(attrs);
106
+
107
+ // add this newsletter last
108
+ const sortOrder = await this.NewsletterModel.getNextAvailableSortOrder(options);
109
+ cleanedAttrs.sort_order = sortOrder;
110
+
111
+ // add the model now because we need the ID for sending verification emails
112
+ const newsletter = await this.NewsletterModel.add(cleanedAttrs, options);
113
+
114
+ // subscribe existing members if opt_in_existing=true
115
+ if (options.opt_in_existing) {
116
+ debug(`Subscribing members to newsletter '${newsletter.get('name')}'`);
117
+
118
+ // subscribe members that have an existing subscription to an active newsletter
119
+ const memberIds = await this.MemberModel.fetchAllSubscribed(_.pick(options, 'transacting'));
120
+
121
+ newsletter.meta = newsletter.meta || {};
122
+ newsletter.meta.opted_in_member_count = memberIds.length;
123
+
124
+ if (memberIds.length) {
125
+ debug(`Found ${memberIds.length} members to subscribe`);
126
+
127
+ await newsletter.subscribeMembersById(memberIds, options);
128
+ }
129
+ }
130
+
131
+ // send any verification emails and respond with the appropriate meta added
132
+ return this.respondWithEmailVerification(newsletter, emailsToVerify);
133
+ }
134
+
135
+ /**
136
+ * @public
137
+ * @param {object} attrs model properties
138
+ * @param {Object} [options] options
139
+ * @returns {Promise<{object}>} Newsetter Model with verification metadata
140
+ */
141
+ async edit(attrs, options = {}) {
142
+ // fetch newsletter first so we can compare changed emails
143
+ const originalNewsletter = await this.NewsletterModel.findOne(options, {require: true});
144
+
145
+ const {cleanedAttrs, emailsToVerify} = await this.prepAttrsForEmailVerification(attrs, originalNewsletter);
146
+
147
+ const updatedNewsletter = await this.NewsletterModel.edit(cleanedAttrs, options);
148
+
149
+ return this.respondWithEmailVerification(updatedNewsletter, emailsToVerify);
150
+ }
151
+
152
+ /**
153
+ * @public
154
+ * @param {string} token - token that provides details of what to update
155
+ * @returns {Promise<{object}>} Newsetter Model
156
+ */
157
+ async verifyPropertyUpdate(token) {
158
+ const data = await this.magicLinkService.getDataFromToken(token);
159
+ const {id, property, value} = data;
160
+
161
+ const attrs = {};
162
+ attrs[property] = value;
163
+
164
+ return this.NewsletterModel.edit(attrs, {id});
165
+ }
166
+
167
+ /* Email verification Internals */
168
+
169
+ /**
170
+ * @private
171
+ */
172
+ async prepAttrsForEmailVerification(attrs, newsletter) {
173
+ const cleanedAttrs = _.cloneDeep(attrs);
174
+ const emailsToVerify = [];
175
+
176
+ for (const property of ['sender_email']) {
177
+ const email = cleanedAttrs[property];
178
+ const hasChanged = !newsletter || newsletter.get(property) !== email;
179
+
180
+ if (await this.requiresEmailVerification({email, hasChanged})) {
181
+ delete cleanedAttrs[property];
182
+ emailsToVerify.push({email, property});
183
+ }
184
+ }
185
+
186
+ return {cleanedAttrs, emailsToVerify};
187
+ }
188
+
189
+ /**
190
+ * @private
191
+ */
192
+ async requiresEmailVerification({email, hasChanged}) {
193
+ if (!email || !hasChanged) {
194
+ return false;
195
+ }
196
+
197
+ // TODO: check other newsletters for known/verified email
198
+
199
+ return true;
200
+ }
201
+
202
+ /**
203
+ * @private
204
+ */
205
+ async respondWithEmailVerification(newsletter, emailsToVerify) {
206
+ if (emailsToVerify.length > 0) {
207
+ for (const {email, property} of emailsToVerify) {
208
+ await this.sendEmailVerificationMagicLink({id: newsletter.get('id'), email, property});
209
+ }
210
+
211
+ newsletter.meta = newsletter.meta || {};
212
+ newsletter.meta.sent_email_verification = emailsToVerify.map(v => v.property);
213
+ }
214
+
215
+ return newsletter;
216
+ }
217
+
218
+ /**
219
+ * @private
220
+ */
221
+ async sendEmailVerificationMagicLink({id, email, property = 'sender_from'}) {
222
+ const [,toDomain] = email.split('@');
223
+
224
+ let fromEmail = `noreply@${toDomain}`;
225
+ if (fromEmail === email) {
226
+ fromEmail = `no-reply@${toDomain}`;
227
+ }
228
+
229
+ const {ghostMailer} = this;
230
+
231
+ this.magicLinkService.transporter = {
232
+ sendMail(message) {
233
+ if (process.env.NODE_ENV !== 'production') {
234
+ logging.warn(message.text);
235
+ }
236
+ let msg = Object.assign({
237
+ from: fromEmail,
238
+ subject: 'Verify email address',
239
+ forceTextContent: true
240
+ }, message);
241
+
242
+ return ghostMailer.send(msg);
243
+ }
244
+ };
245
+
246
+ return this.magicLinkService.sendMagicLink({email, tokenData: {id, property, value: email}});
247
+ }
21
248
  }
22
249
 
23
- module.exports = NewslettersService;
250
+ /**
251
+ * @typedef {object} ILimitService
252
+ * @prop {(name: string) => Promise<void>} errorIfWouldGoOverLimit
253
+ **/
24
254
 
255
+ module.exports = NewslettersService;
@@ -4,7 +4,8 @@ const tpl = require('@tryghost/tpl');
4
4
 
5
5
  const messages = {
6
6
  invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.',
7
- invalidVisibilityFilter: 'Invalid visibility filter.'
7
+ invalidVisibilityFilter: 'Invalid visibility filter.',
8
+ invalidNewsletterId: 'The newsletter_id parameter doesn\'t match any active newsletter.'
8
9
  };
9
10
 
10
11
  class PostsService {
@@ -19,6 +20,22 @@ class PostsService {
19
20
  async editPost(frame) {
20
21
  let model;
21
22
 
23
+ // Make sure the newsletter_id is matching an active newsletter
24
+ if (frame.options.newsletter_id) {
25
+ const newsletter = await this.models.Newsletter.findOne({id: frame.options.newsletter_id, filter: 'status:active'}, {transacting: frame.options.transacting});
26
+ if (!newsletter) {
27
+ throw new BadRequestError({
28
+ message: messages.invalidNewsletterId
29
+ });
30
+ }
31
+ } else {
32
+ // Set the newsletter_id if it isn't passed to the API
33
+ const newsletters = await this.models.Newsletter.findPage({filter: 'status:active', limit: 1, columns: ['id']}, {transacting: frame.options.transacting});
34
+ if (newsletters.data.length > 0) {
35
+ frame.options.newsletter_id = newsletters.data[0].id;
36
+ }
37
+ }
38
+
22
39
  if (!frame.options.email_recipient_filter && frame.options.send_email_when_published) {
23
40
  await this.models.Base.transaction(async (transacting) => {
24
41
  const options = {
@@ -1,8 +1,4 @@
1
+ const StatsService = require('@tryghost/stats-service');
1
2
  const db = require('../../data/db');
2
- const MemberStatsService = require('./lib/members-stats-service');
3
- const MrrStatsService = require('./lib/mrr-stats-service');
4
3
 
5
- module.exports = {
6
- members: new MemberStatsService({db}),
7
- mrr: new MrrStatsService({db})
8
- };
4
+ module.exports = StatsService.create({knex: db.knex});
@@ -73,28 +73,28 @@ class Users {
73
73
  const parsedFileName = path.parse(backupPath);
74
74
  const filename = `${parsedFileName.name}${parsedFileName.ext}`;
75
75
 
76
- return this.models.Base.transaction((t) => {
76
+ return this.models.Base.transaction(async (t) => {
77
77
  frameOptions.transacting = t;
78
78
 
79
- return this.models.Post.destroyByAuthor(frameOptions)
80
- .then(() => {
81
- return this.models.ApiKey.destroy({
82
- ...frameOptions,
83
- require: true,
84
- destroyBy: {
85
- user_id: frameOptions.id
86
- }
87
- }).catch((err) => {
88
- if (err instanceof this.models.ApiKey.NotFoundError) {
89
- return; //Do nothing here as it's ok
90
- }
91
- throw err;
92
- });
93
- })
94
- .then(() => {
95
- return this.models.User.destroy(Object.assign({status: 'all'}, frameOptions));
96
- })
97
- .then(() => filename);
79
+ await this.models.Post.destroyByAuthor(frameOptions);
80
+
81
+ try {
82
+ await this.models.ApiKey.destroy({
83
+ ...frameOptions,
84
+ require: true,
85
+ destroyBy: {
86
+ user_id: frameOptions.id
87
+ }
88
+ });
89
+ } catch (err) {
90
+ if (!(err instanceof this.models.ApiKey.NotFoundError)) {
91
+ throw err;
92
+ }
93
+ }
94
+
95
+ await this.models.User.destroy(Object.assign({status: 'all'}, frameOptions));
96
+
97
+ return filename;
98
98
  });
99
99
  }
100
100
  }
@@ -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%2F%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%224.43%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%2F%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%224.46%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" />
@@ -38,7 +38,7 @@
38
38
 
39
39
 
40
40
  <link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
41
- <link rel="stylesheet" href="assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-bd8cd0185fd5dfc8291502f801e443e6.css" title="light">
42
42
 
43
43
 
44
44
 
@@ -56,8 +56,8 @@
56
56
  <div id="ember-basic-dropdown-wormhole"></div>
57
57
 
58
58
 
59
- <script src="assets/vendor.min-21f79c68a284acb1b70039f3f63e5507.js"></script>
60
- <script src="assets/ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js"></script>
59
+ <script src="assets/vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js"></script>
60
+ <script src="assets/ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -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%2F%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%224.43%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%2F%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%224.46%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" />
@@ -38,7 +38,7 @@
38
38
 
39
39
 
40
40
  <link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
41
- <link rel="stylesheet" href="assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-bd8cd0185fd5dfc8291502f801e443e6.css" title="light">
42
42
 
43
43
 
44
44
 
@@ -56,8 +56,8 @@
56
56
  <div id="ember-basic-dropdown-wormhole"></div>
57
57
 
58
58
 
59
- <script src="assets/vendor.min-21f79c68a284acb1b70039f3f63e5507.js"></script>
60
- <script src="assets/ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js"></script>
59
+ <script src="assets/vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js"></script>
60
+ <script src="assets/ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -4,6 +4,8 @@ const express = require('../../../shared/express');
4
4
  const urlUtils = require('../../../shared/url-utils');
5
5
  const sentry = require('../../../shared/sentry');
6
6
  const errorHandler = require('@tryghost/mw-error-handler');
7
+ const versionMissmatchHandler = require('@tryghost/mw-api-version-mismatch');
8
+ const {APIVersionCompatibilityServiceInstance} = require('../../services/api-version-compatibility');
7
9
 
8
10
  module.exports = function setupApiApp() {
9
11
  debug('Parent API setup start');
@@ -30,6 +32,7 @@ module.exports = function setupApiApp() {
30
32
 
31
33
  // Error handling for requests to non-existent API versions
32
34
  apiApp.use(errorHandler.resourceNotFound);
35
+ apiApp.use(versionMissmatchHandler(APIVersionCompatibilityServiceInstance));
33
36
  apiApp.use(errorHandler.handleJSONResponse(sentry));
34
37
 
35
38
  debug('Parent API setup end');
@@ -5,8 +5,10 @@ const bodyParser = require('body-parser');
5
5
  const shared = require('../../../shared');
6
6
  const apiMw = require('../../middleware');
7
7
  const errorHandler = require('@tryghost/mw-error-handler');
8
+ const versionMissmatchHandler = require('@tryghost/mw-api-version-mismatch');
8
9
  const sentry = require('../../../../../shared/sentry');
9
10
  const routes = require('./routes');
11
+ const {APIVersionCompatibilityServiceInstance} = require('../../../../services/api-version-compatibility');
10
12
 
11
13
  module.exports = function setupApiApp() {
12
14
  debug('Admin API canary setup start');
@@ -33,6 +35,7 @@ module.exports = function setupApiApp() {
33
35
 
34
36
  // API error handling
35
37
  apiApp.use(errorHandler.resourceNotFound);
38
+ apiApp.use(versionMissmatchHandler(APIVersionCompatibilityServiceInstance));
36
39
  apiApp.use(errorHandler.handleJSONResponseV2(sentry));
37
40
 
38
41
  debug('Admin API canary setup end');