ghost 5.3.1 → 5.5.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 (107) hide show
  1. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  2. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  3. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  4. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  5. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  6. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  7. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  8. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  9. package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
  10. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  11. package/components/tryghost-members-csv-0.0.0.tgz +0 -0
  12. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  13. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  14. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  15. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  16. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  17. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  18. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  19. package/content/themes/casper/assets/built/global.css +1 -1
  20. package/content/themes/casper/assets/built/global.css.map +1 -1
  21. package/content/themes/casper/assets/built/screen.css +1 -1
  22. package/content/themes/casper/assets/built/screen.css.map +1 -1
  23. package/content/themes/casper/assets/css/screen.css +31 -8
  24. package/content/themes/casper/default.hbs +8 -5
  25. package/content/themes/casper/gulpfile.js +1 -1
  26. package/content/themes/casper/package.json +9 -9
  27. package/content/themes/casper/yarn.lock +1154 -1249
  28. package/core/boot.js +5 -0
  29. package/core/built/assets/{chunk.3.dc389a0f93cb5fabd695.js → chunk.3.550552fbc71864fb9738.js} +20 -20
  30. package/core/built/assets/fonts/Inter.ttf +0 -0
  31. package/core/built/assets/ghost-dark-5c2a961b35311d7298136e02289d98b2.css +1 -0
  32. package/core/built/assets/ghost.min-a89d10b3b58c1a5ebaca68cef93a404c.css +1 -0
  33. package/core/built/assets/{ghost.min-f4bba3a2a5ef256b82641345505d4f0f.js → ghost.min-c75f224decd20f9538179d7564cd2ab4.js} +3025 -2883
  34. package/core/built/assets/icons/event-comment.svg +3 -0
  35. package/core/built/assets/{vendor.min-4076498ccd6c8412365f43b156084ed8.js → vendor.min-cf3af99dca0c71937669305afb3686a1.js} +6122 -3197
  36. package/core/frontend/helpers/comments.js +22 -10
  37. package/core/frontend/helpers/ghost_head.js +22 -4
  38. package/core/frontend/helpers/total_members.js +17 -0
  39. package/core/frontend/helpers/total_paid_members.js +16 -0
  40. package/core/frontend/utils/frontend-apps.js +33 -0
  41. package/core/frontend/utils/member-count.js +50 -0
  42. package/core/frontend/web/middleware/cors.js +2 -1
  43. package/core/server/api/endpoints/comments-comments.js +50 -32
  44. package/core/server/api/endpoints/offers-public.js +2 -2
  45. package/core/server/api/endpoints/offers.js +8 -8
  46. package/core/server/api/endpoints/settings.js +62 -30
  47. package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
  48. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  49. package/core/server/api/endpoints/utils/serializers/output/index.js +0 -4
  50. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +17 -0
  51. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +18 -0
  52. package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +2 -0
  53. package/core/server/api/endpoints/utils/serializers/output/mappers/offers.js +28 -0
  54. package/core/server/api/endpoints/utils/serializers/output/members.js +12 -1
  55. package/core/server/api/endpoints/utils/serializers/output/settings.js +2 -1
  56. package/core/server/api/endpoints/utils/validators/input/settings.js +22 -2
  57. package/core/server/data/exporter/table-lists.js +2 -1
  58. package/core/server/data/migrations/versions/5.5/2022-07-18-14-29-add-comment-reporting-permissions.js +10 -0
  59. package/core/server/data/migrations/versions/5.5/2022-07-18-14-31-drop-reports-reason.js +3 -0
  60. package/core/server/data/migrations/versions/5.5/2022-07-18-14-32-drop-nullable-member-id-from-likes.js +4 -0
  61. package/core/server/data/migrations/versions/5.5/2022-07-18-14-33-fix-comments-on-delete-foreign-keys.js +119 -0
  62. package/core/server/data/migrations/versions/5.5/2022-07-21-08-56-add-jobs-table.js +11 -0
  63. package/core/server/data/schema/commands.js +7 -2
  64. package/core/server/data/schema/fixtures/fixtures.json +5 -0
  65. package/core/server/data/schema/schema.js +12 -4
  66. package/core/server/ghost-server.js +0 -22
  67. package/core/server/models/comment-report.js +34 -0
  68. package/core/server/models/comment.js +8 -7
  69. package/core/server/models/job.js +9 -0
  70. package/core/server/models/tag.js +4 -0
  71. package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
  72. package/core/server/services/comments/email-templates/new-comment-reply.txt.js +7 -8
  73. package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
  74. package/core/server/services/comments/email-templates/new-comment.txt.js +7 -6
  75. package/core/server/services/comments/email-templates/report.hbs +199 -0
  76. package/core/server/services/comments/email-templates/report.txt.js +16 -0
  77. package/core/server/services/comments/emails.js +57 -1
  78. package/core/server/services/comments/service.js +194 -2
  79. package/core/server/services/jobs/job-service.js +24 -1
  80. package/core/server/services/mail/GhostMailer.js +1 -0
  81. package/core/server/services/members/SingleUseTokenProvider.js +3 -3
  82. package/core/server/services/members/api.js +2 -1
  83. package/core/server/services/members/config.js +4 -1
  84. package/core/server/services/members/middleware.js +14 -2
  85. package/core/server/services/members/settings.js +4 -90
  86. package/core/server/services/public-config/config.js +2 -1
  87. package/core/server/services/settings/emails/verify-email.js +166 -0
  88. package/core/server/services/settings/settings-bread-service.js +170 -4
  89. package/core/server/services/settings/settings-service.js +9 -1
  90. package/core/server/services/stripe/service.js +9 -1
  91. package/core/server/services/webhooks/serialize.js +5 -0
  92. package/core/server/web/admin/views/default-prod.html +4 -4
  93. package/core/server/web/admin/views/default.html +4 -4
  94. package/core/server/web/api/endpoints/admin/routes.js +6 -0
  95. package/core/server/web/api/endpoints/content/routes.js +2 -1
  96. package/core/server/web/api/middleware/cors.js +2 -1
  97. package/core/server/web/api/testmode/jobs/graceful-job.js +2 -2
  98. package/core/server/web/api/testmode/routes.js +14 -0
  99. package/core/server/web/comments/routes.js +2 -0
  100. package/core/server/web/members/app.js +2 -4
  101. package/core/shared/config/defaults.json +15 -7
  102. package/core/shared/config/env/config.testing.json +3 -2
  103. package/package.json +75 -60
  104. package/yarn.lock +1812 -1832
  105. package/core/built/assets/ghost-dark-9e5d1f0dfae41232e5e34e4d0df53ae0.css +0 -1
  106. package/core/built/assets/ghost.min-e7cfbd1800f8e99b9158f74f1e39cd76.css +0 -1
  107. package/core/server/api/endpoints/utils/serializers/output/offers.js +0 -16
@@ -1,99 +1,14 @@
1
- const MagicLink = require('@tryghost/magic-link');
2
- const {URL} = require('url');
3
- const path = require('path');
4
1
  const urlUtils = require('../../../shared/url-utils');
5
- const settingsCache = require('../../../shared/settings-cache');
6
- const logging = require('@tryghost/logging');
7
- const mail = require('../mail');
8
- const updateEmailTemplate = require('./emails/updateEmail');
9
2
  const SingleUseTokenProvider = require('./SingleUseTokenProvider');
10
3
  const models = require('../../models');
11
4
  const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
12
5
 
13
- const ghostMailer = new mail.GhostMailer();
14
-
15
- function createSettingsInstance(config) {
16
- const {transporter, getSubject, getText, getHTML, getSigninURL} = {
17
- transporter: {
18
- sendMail(message) {
19
- if (process.env.NODE_ENV !== 'production') {
20
- logging.warn(message.text);
21
- }
22
- let msg = Object.assign({
23
- from: config.getAuthEmailFromAddress(),
24
- subject: 'Update email address',
25
- forceTextContent: true
26
- }, message);
27
-
28
- return ghostMailer.send(msg);
29
- }
30
- },
31
- getSubject() {
32
- return `Confirm your email address`;
33
- },
34
- getText(url, type, email) {
35
- return `
36
- Hey there,
37
-
38
- Please confirm your email address with this link:
39
-
40
- ${url}
41
-
42
- For your security, the link will expire in 24 hours time.
43
-
44
- ---
45
-
46
- Sent to ${email}
47
- If you did not make this request, you can simply delete this message. This email address will not be used.
48
- `;
49
- },
50
- getHTML(url, type, email) {
51
- const siteTitle = settingsCache.get('title');
52
- return updateEmailTemplate({url, email, siteTitle});
53
- },
54
- getSigninURL(token, type) {
55
- const signinURL = new URL(urlUtils.urlFor('api', {type: 'admin'}, true));
56
- signinURL.pathname = path.join(signinURL.pathname, '/settings/members/email/');
57
- signinURL.searchParams.set('token', token);
58
- signinURL.searchParams.set('action', type);
59
- return signinURL.href;
60
- }
61
- };
62
-
63
- const magicLinkService = new MagicLink({
64
- transporter,
65
- tokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY),
66
- getSigninURL,
67
- getText,
68
- getHTML,
69
- getSubject
70
- });
71
-
72
- const sendEmailAddressUpdateMagicLink = ({email, type = 'supportAddressUpdate'}) => {
73
- const [,toDomain] = email.split('@');
74
- let fromEmail = `noreply@${toDomain}`;
75
- if (fromEmail === email) {
76
- fromEmail = `no-reply@${toDomain}`;
77
- }
78
- magicLinkService.transporter = {
79
- sendMail(message) {
80
- if (process.env.NODE_ENV !== 'production') {
81
- logging.warn(message.text);
82
- }
83
- let msg = Object.assign({
84
- from: fromEmail,
85
- subject: 'Update email address',
86
- forceTextContent: true
87
- }, message);
88
-
89
- return ghostMailer.send(msg);
90
- }
91
- };
92
- return magicLinkService.sendMagicLink({email, tokenData: {email}, subject: email, type});
93
- };
6
+ // @todo: can get removed, since this is moved to the settings bread service
7
+ function createSettingsInstance() {
8
+ const oldTokenProvider = new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY);
94
9
 
95
10
  const getEmailFromToken = async ({token}) => {
96
- const data = await magicLinkService.getDataFromToken(token);
11
+ const data = await oldTokenProvider.validate(token);
97
12
  return data.email;
98
13
  };
99
14
 
@@ -107,7 +22,6 @@ function createSettingsInstance(config) {
107
22
  };
108
23
 
109
24
  return {
110
- sendEmailAddressUpdateMagicLink,
111
25
  getEmailFromToken,
112
26
  getAdminRedirectLink
113
27
  };
@@ -18,7 +18,8 @@ module.exports = function getConfigProperties() {
18
18
  mailgunIsConfigured: !!(config.get('bulkEmail') && config.get('bulkEmail').mailgun),
19
19
  emailAnalytics: config.get('emailAnalytics'),
20
20
  hostSettings: config.get('hostSettings'),
21
- tenor: config.get('tenor')
21
+ tenor: config.get('tenor'),
22
+ editor: config.get('editor')
22
23
  };
23
24
 
24
25
  const billingUrl = config.get('hostSettings:billing:enabled') ? config.get('hostSettings:billing:url') : '';
@@ -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,8 +1,12 @@
1
1
  const _ = require('lodash');
2
2
  const tpl = require('@tryghost/tpl');
3
- const {NotFoundError, NoPermissionError, BadRequestError} = require('@tryghost/errors');
3
+ const {NotFoundError, NoPermissionError, BadRequestError, IncorrectUsageError} = require('@tryghost/errors');
4
4
  const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
5
+ const logging = require('@tryghost/logging');
6
+ const MagicLink = require('@tryghost/magic-link');
7
+ const verifyEmailTemplate = require('./emails/verify-email');
5
8
 
9
+ const EMAIL_KEYS = ['members_support_address'];
6
10
  const messages = {
7
11
  problemFindingSetting: 'Problem finding setting: {key}',
8
12
  accessCoreSettingFromExtReq: 'Attempted to access core setting from external request'
@@ -13,13 +17,67 @@ class SettingsBREADService {
13
17
  *
14
18
  * @param {Object} options
15
19
  * @param {Object} options.SettingsModel
20
+ * @param {Object} options.mail
16
21
  * @param {Object} options.settingsCache - SettingsCache instance
22
+ * @param {Object} options.singleUseTokenProvider
23
+ * @param {Object} options.urlUtils
17
24
  * @param {Object} options.labsService - labs service instance
18
25
  */
19
- constructor({SettingsModel, settingsCache, labsService}) {
26
+ constructor({SettingsModel, settingsCache, labsService, mail, singleUseTokenProvider, urlUtils}) {
20
27
  this.SettingsModel = SettingsModel;
21
28
  this.settingsCache = settingsCache;
22
29
  this.labs = labsService;
30
+
31
+ /* email verification setup */
32
+
33
+ this.ghostMailer = new mail.GhostMailer();
34
+
35
+ const {transporter, getSubject, getText, getHTML, getSigninURL} = {
36
+ transporter: {
37
+ sendMail() {
38
+ // noop - overridden in `sendEmailVerificationMagicLink`
39
+ }
40
+ },
41
+ getSubject() {
42
+ // not used - overridden in `sendEmailVerificationMagicLink`
43
+ return `Verify email address`;
44
+ },
45
+ getText(url, type, email) {
46
+ return `
47
+ Hey there,
48
+
49
+ Please confirm your email address with this link:
50
+
51
+ ${url}
52
+
53
+ For your security, the link will expire in 24 hours time.
54
+
55
+ ---
56
+
57
+ Sent to ${email}
58
+ If you did not make this request, you can simply delete this message. This email address will not be used.
59
+ `;
60
+ },
61
+ getHTML(url, type, email) {
62
+ return verifyEmailTemplate({url, email});
63
+ },
64
+ getSigninURL(token) {
65
+ // @todo: need to make this more generic?
66
+ const adminUrl = urlUtils.urlFor('admin', true);
67
+ const signinURL = new URL(adminUrl);
68
+ signinURL.hash = `/settings/members/?verifyEmail=${token}`;
69
+ return signinURL.href;
70
+ }
71
+ };
72
+
73
+ this.magicLinkService = new MagicLink({
74
+ transporter,
75
+ tokenProvider: singleUseTokenProvider,
76
+ getSigninURL,
77
+ getText,
78
+ getHTML,
79
+ getSubject
80
+ });
23
81
  }
24
82
 
25
83
  /**
@@ -93,7 +151,7 @@ class SettingsBREADService {
93
151
  * @returns
94
152
  */
95
153
  async edit(settings, options, stripeConnectData) {
96
- const filteredSettings = settings.filter((setting) => {
154
+ let filteredSettings = settings.filter((setting) => {
97
155
  // The `stripe_connect_integration_token` "setting" is only used to set the `stripe_connect_*` settings.
98
156
  return ![
99
157
  'stripe_connect_integration_token',
@@ -152,9 +210,31 @@ class SettingsBREADService {
152
210
  });
153
211
  }
154
212
 
155
- return this.SettingsModel.edit(filteredSettings, options).then((result) => {
213
+ // remove any email properties that are not allowed to be set without verification
214
+ const {filteredSettings: refilteredSettings, emailsToVerify} = await this.prepSettingsForEmailVerification(filteredSettings, getSetting);
215
+
216
+ const modelArray = await this.SettingsModel.edit(refilteredSettings, options).then((result) => {
156
217
  return this._formatBrowse(_.keyBy(_.invokeMap(result, 'toJSON'), 'key'), options.context);
157
218
  });
219
+
220
+ return this.respondWithEmailVerification(modelArray, emailsToVerify);
221
+ }
222
+
223
+ async verifyKeyUpdate(token) {
224
+ const data = await this.magicLinkService.getDataFromToken(token);
225
+ const {key, value} = data;
226
+
227
+ // Verify keys (in case they ever change and we have old tokens)
228
+ if (!EMAIL_KEYS.includes(key)) {
229
+ throw new IncorrectUsageError({
230
+ message: 'Not allowed to update this setting key via tokens'
231
+ });
232
+ }
233
+
234
+ return this.SettingsModel.edit({
235
+ key,
236
+ value
237
+ });
158
238
  }
159
239
 
160
240
  /**
@@ -205,6 +285,92 @@ class SettingsBREADService {
205
285
 
206
286
  return settings;
207
287
  }
288
+
289
+ /**
290
+ * @private
291
+ */
292
+ async prepSettingsForEmailVerification(settings, getSetting) {
293
+ const filteredSettings = [];
294
+ const emailsToVerify = [];
295
+
296
+ for (const setting of settings) {
297
+ if (EMAIL_KEYS.includes(setting.key)) {
298
+ const email = setting.value;
299
+ const key = setting.key;
300
+ const hasChanged = getSetting(setting).value !== email;
301
+
302
+ if (await this.requiresEmailVerification({email, hasChanged})) {
303
+ emailsToVerify.push({email, key});
304
+ } else {
305
+ filteredSettings.push(setting);
306
+ }
307
+ } else {
308
+ filteredSettings.push(setting);
309
+ }
310
+ }
311
+
312
+ return {filteredSettings, emailsToVerify};
313
+ }
314
+
315
+ /**
316
+ * @private
317
+ */
318
+ async requiresEmailVerification({email, hasChanged}) {
319
+ if (!email || !hasChanged || email === 'noreply') {
320
+ return false;
321
+ }
322
+
323
+ // TODO: check for known/verified email
324
+
325
+ return true;
326
+ }
327
+
328
+ /**
329
+ * @private
330
+ */
331
+ async respondWithEmailVerification(settings, emailsToVerify) {
332
+ if (emailsToVerify.length > 0) {
333
+ for (const {email, key} of emailsToVerify) {
334
+ await this.sendEmailVerificationMagicLink({email, key});
335
+ }
336
+
337
+ settings.meta = settings.meta || {};
338
+ settings.meta.sent_email_verification = emailsToVerify.map(v => v.key);
339
+ }
340
+
341
+ return settings;
342
+ }
343
+
344
+ /**
345
+ * @private
346
+ */
347
+ async sendEmailVerificationMagicLink({email, key}) {
348
+ const [,toDomain] = email.split('@');
349
+
350
+ let fromEmail = `noreply@${toDomain}`;
351
+ if (fromEmail === email) {
352
+ fromEmail = `no-reply@${toDomain}`;
353
+ }
354
+
355
+ const {ghostMailer} = this;
356
+
357
+ this.magicLinkService.transporter = {
358
+ sendMail(message) {
359
+ if (process.env.NODE_ENV !== 'production') {
360
+ logging.warn(message.text);
361
+ }
362
+ let msg = Object.assign({
363
+ from: fromEmail,
364
+ subject: 'Verify email address',
365
+ forceTextContent: true
366
+ }, message);
367
+
368
+ return ghostMailer.send(msg);
369
+ }
370
+ };
371
+
372
+ return this.magicLinkService.sendMagicLink({email, tokenData: {key, value: email}});
373
+ }
208
374
  }
209
375
 
210
376
  module.exports = SettingsBREADService;
@@ -12,6 +12,9 @@ const config = require('../../../shared/config');
12
12
  const SettingsCache = require('../../../shared/settings-cache');
13
13
  const SettingsBREADService = require('./settings-bread-service');
14
14
  const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
15
+ const mail = require('../mail');
16
+ const SingleUseTokenProvider = require('../members/SingleUseTokenProvider');
17
+ const urlUtils = require('../../../shared/url-utils');
15
18
 
16
19
  const ObjectId = require('bson-objectid');
17
20
 
@@ -19,6 +22,8 @@ const messages = {
19
22
  incorrectKeyType: 'type must be one of "direct" or "connect".'
20
23
  };
21
24
 
25
+ const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
26
+
22
27
  /**
23
28
  * @returns {SettingsBREADService} instance of the PostsService
24
29
  */
@@ -26,7 +31,10 @@ const getSettingsBREADServiceInstance = () => {
26
31
  return new SettingsBREADService({
27
32
  SettingsModel: models.Settings,
28
33
  settingsCache: SettingsCache,
29
- labsService: labs
34
+ labsService: labs,
35
+ mail,
36
+ singleUseTokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY),
37
+ urlUtils
30
38
  });
31
39
  };
32
40
 
@@ -25,7 +25,15 @@ const debouncedConfigureApi = _.debounce(() => {
25
25
 
26
26
  module.exports = new StripeService({
27
27
  membersService,
28
- models: _.pick(models, ['Product', 'StripePrice', 'StripeCustomerSubscription', 'StripeProduct', 'MemberStripeCustomer', 'Offer', 'Settings']),
28
+ models: _.pick(models, [
29
+ 'Product',
30
+ 'StripePrice',
31
+ 'StripeCustomerSubscription',
32
+ 'StripeProduct',
33
+ 'MemberStripeCustomer',
34
+ 'Offer',
35
+ 'Settings'
36
+ ]),
29
37
  StripeWebhook: {
30
38
  async get() {
31
39
  return {
@@ -13,9 +13,14 @@ module.exports = (event, model) => {
13
13
  ops.push(() => {
14
14
  let frame = {options: {previous: false, context: {user: true}}};
15
15
 
16
+ // NOTE: below options are lost in the during event processing, a more holistic approach would be
17
+ // to pass them somehow along with the model
16
18
  if (['posts', 'pages'].includes(docName)) {
17
19
  frame.options.formats = ['mobiledoc', 'html', 'plaintext'];
18
20
  frame.options.withRelated = ['tags', 'authors'];
21
+ model._originalOptions = {
22
+ withRelated: ['tags', 'authors']
23
+ };
19
24
  }
20
25
 
21
26
  return apiShared
@@ -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%225.3%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%225.5%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-4a6661c574707ceca220aa2e76558995.css">
41
- <link rel="stylesheet" href="assets/ghost.min-e7cfbd1800f8e99b9158f74f1e39cd76.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-a89d10b3b58c1a5ebaca68cef93a404c.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-4076498ccd6c8412365f43b156084ed8.js"></script>
60
- <script src="assets/ghost.min-f4bba3a2a5ef256b82641345505d4f0f.js"></script>
59
+ <script src="assets/vendor.min-cf3af99dca0c71937669305afb3686a1.js"></script>
60
+ <script src="assets/ghost.min-c75f224decd20f9538179d7564cd2ab4.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%225.3%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%225.5%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-4a6661c574707ceca220aa2e76558995.css">
41
- <link rel="stylesheet" href="assets/ghost.min-e7cfbd1800f8e99b9158f74f1e39cd76.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-a89d10b3b58c1a5ebaca68cef93a404c.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-4076498ccd6c8412365f43b156084ed8.js"></script>
60
- <script src="assets/ghost.min-f4bba3a2a5ef256b82641345505d4f0f.js"></script>
59
+ <script src="assets/vendor.min-cf3af99dca0c71937669305afb3686a1.js"></script>
60
+ <script src="assets/ghost.min-c75f224decd20f9538179d7564cd2ab4.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -64,8 +64,14 @@ module.exports = function apiRoutes() {
64
64
 
65
65
  router.get('/settings', mw.authAdminApi, http(api.settings.browse));
66
66
  router.put('/settings', mw.authAdminApi, http(api.settings.edit));
67
+ router.put('/settings/verifications/', mw.authAdminApi, http(api.settings.verifyKeyUpdate));
68
+
69
+ /** @deprecated This endpoint is part of the old email verification flow for the support email */
67
70
  router.get('/settings/members/email', http(api.settings.validateMembersEmailUpdate));
71
+
72
+ /** @deprecated This endpoint is part of the old email verification flow for the support email */
68
73
  router.post('/settings/members/email', mw.authAdminApi, http(api.settings.updateMembersEmail));
74
+
69
75
  router.del('/settings/stripe/connect', mw.authAdminApi, http(api.settings.disconnectStripeConnectIntegration));
70
76
 
71
77
  // ## Users
@@ -3,11 +3,12 @@ const cors = require('cors');
3
3
  const api = require('../../../../api').endpoints;
4
4
  const http = require('../../../../api').shared.http;
5
5
  const mw = require('./middleware');
6
+ const config = require('../../../../../shared/config');
6
7
 
7
8
  module.exports = function apiRoutes() {
8
9
  const router = express.Router('content api');
9
10
 
10
- router.use(cors());
11
+ router.use(cors({maxAge: config.get('caching:cors:maxAge')}));
11
12
 
12
13
  // ## Posts
13
14
  router.get('/posts', mw.authenticatePublic, http(api.postsPublic.browse));