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.
- package/Gruntfile.js +1 -1
- package/core/boot.js +2 -0
- package/core/built/assets/{chunk.3.6e2ed2d00856e12bd81a.js → chunk.3.52b444495dfcf50afb0b.js} +20 -20
- package/core/built/assets/ghost-dark-155e039c0d991b7af75dea8cd3846b11.css +1 -0
- package/core/built/assets/{ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js → ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js} +845 -670
- package/core/built/assets/ghost.min-bd8cd0185fd5dfc8291502f801e443e6.css +1 -0
- package/core/built/assets/icons/clock.svg +1 -1
- package/core/built/assets/icons/email-at.svg +1 -0
- package/core/built/assets/icons/email-body.svg +1 -0
- package/core/built/assets/icons/email-footer.svg +1 -0
- package/core/built/assets/icons/email-header.svg +1 -0
- package/core/built/assets/icons/email-member.svg +1 -0
- package/core/built/assets/icons/email-name.svg +1 -0
- package/core/built/assets/icons/member.svg +1 -3
- package/core/built/assets/icons/send-email.svg +1 -1
- package/core/built/assets/img/abstract-2-2937e2902b64360d0cbe4cec8bd8479b.jpg +0 -0
- package/core/built/assets/img/abstract-c52b2f4208e7fd2e7b8abd8b1eec4f7b.jpg +0 -0
- package/core/built/assets/img/community-background-3f501ff1d764d0cb81f7c2cbacfc6503.jpg +0 -0
- package/core/built/assets/img/community-be8c1dcecfb157f2bfba5cababc8e686.jpg +0 -0
- package/core/built/assets/img/newsletter-1-197ae8063dfb2e22278d355198029c9e.jpg +0 -0
- package/core/built/assets/img/newsletter-2-5a2c7693ea9380d4282061302c01267a.jpg +0 -0
- package/core/built/assets/img/resource-1-722f202795856e4a5596c8a3b7bedc43.jpg +0 -0
- package/core/built/assets/{vendor.min-21f79c68a284acb1b70039f3f63e5507.js → vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js} +868 -523
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -3
- package/core/frontend/apps/amp/lib/views/amp.hbs +5 -3
- package/core/frontend/helpers/get.js +1 -1
- package/core/frontend/services/routing/controllers/unsubscribe.js +22 -0
- package/core/frontend/web/middleware/cors.js +56 -0
- package/core/frontend/web/middleware/index.js +1 -0
- package/core/frontend/web/middleware/static-theme.js +8 -8
- package/core/frontend/web/site.js +1 -48
- package/core/server/api/canary/authentication.js +2 -2
- package/core/server/api/canary/members.js +3 -0
- package/core/server/api/canary/newsletters.js +86 -4
- package/core/server/api/canary/posts.js +1 -0
- package/core/server/api/canary/stats.js +11 -2
- package/core/server/api/canary/utils/serializers/input/members.js +22 -0
- package/core/server/api/canary/utils/serializers/output/mappers/pages.js +1 -0
- package/core/server/api/canary/utils/serializers/output/mappers/posts.js +2 -0
- package/core/server/api/canary/utils/serializers/output/members.js +13 -2
- package/core/server/api/shared/http.js +1 -1
- package/core/server/api/v2/utils/serializers/output/utils/mapper.js +2 -0
- package/core/server/api/v3/utils/serializers/output/utils/mapper.js +3 -0
- package/core/server/data/importer/importers/data/settings.js +0 -3
- package/core/server/data/migrations/utils.js +40 -0
- package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +5 -5
- package/core/server/data/migrations/versions/4.44/2022-04-06-15-22-populate-type-column-for-paid-subscription-events.js +21 -0
- package/core/server/data/migrations/versions/4.44/2022-04-08-11-54-add-cancelled-events.js +51 -0
- package/core/server/data/migrations/versions/4.44/2022-04-11-08-24-add-newsletter-permissions.js +33 -0
- package/core/server/data/migrations/versions/4.44/2022-04-11-10-54-add-mrr-to-subscriptions.js +8 -0
- package/core/server/data/migrations/versions/4.44/2022-04-12-07-33-fill-mrr.js +29 -0
- package/core/server/data/migrations/versions/4.44/2022-04-13-12-00-remove-newsletter-sender-name-not-null-constraint.js +33 -0
- package/core/server/data/migrations/versions/4.44/2022-04-15-07-53-add-offer-id-to-subscriptions.js +9 -0
- package/core/server/data/migrations/versions/4.45/2022-04-19-12-23-backfill-subscriptions-offers.js +60 -0
- package/core/server/data/migrations/versions/4.45/2022-04-20-11-25-add-newsletter-read-permission.js +9 -0
- package/core/server/data/migrations/versions/4.45/2022-04-21-02-55-add-notifications-key-entry-to-settings-table.js +8 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-00-add-created-at-newsletters.js +6 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-01-add-updated-at-newsletters.js +6 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-02-fill-created-at-newsletters.js +19 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-03-drop-nullable-created-at-newsletters.js +3 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-08-newsletters-show-header-name.js +7 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-57-add-uuid-column-to-newsletters.js +8 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-58-fill-uuid-for-newsletters.js +19 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-59-drop-nullable-uuid-newsletters.js +3 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +85 -0
- package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +66 -0
- package/core/server/data/migrations/versions/4.46/2022-04-22-07-43-add-newsletter-id-to-subscribe-events.js +9 -0
- package/core/server/data/migrations/versions/4.46/2022-04-27-07-59-set-newsletter-id-subscribe-events.js +31 -0
- package/core/server/data/schema/commands.js +14 -0
- package/core/server/data/schema/default-settings/default-settings.json +4 -0
- package/core/server/data/schema/fixtures/fixtures.json +32 -1
- package/core/server/data/schema/schema.js +15 -8
- package/core/server/models/base/plugins/generate-slug.js +2 -2
- package/core/server/models/email.js +4 -0
- package/core/server/models/label.js +1 -1
- package/core/server/models/member-subscribe-event.js +4 -0
- package/core/server/models/member.js +29 -0
- package/core/server/models/newsletter.js +101 -11
- package/core/server/models/post.js +15 -5
- package/core/server/models/role.js +1 -1
- package/core/server/models/stripe-customer-subscription.js +4 -0
- package/core/server/models/tag.js +1 -1
- package/core/server/models/user.js +1 -1
- package/core/server/services/api-version-compatibility/index.js +29 -0
- package/core/server/services/auth/members/index.js +1 -1
- package/core/server/services/auth/setup.js +17 -7
- package/core/server/services/mega/email-preview.js +4 -1
- package/core/server/services/mega/mega.js +86 -27
- package/core/server/services/mega/post-email-serializer.js +17 -14
- package/core/server/services/mega/template.js +24 -3
- package/core/server/services/members/api.js +2 -2
- package/core/server/services/members/middleware.js +69 -2
- package/core/server/services/members/service.js +7 -12
- package/core/server/services/newsletters/emails/verify-email.js +166 -0
- package/core/server/services/newsletters/index.js +14 -7
- package/core/server/services/newsletters/service.js +237 -6
- package/core/server/services/posts/posts-service.js +18 -1
- package/core/server/services/stats/service.js +2 -6
- package/core/server/services/users.js +20 -20
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +3 -0
- package/core/server/web/api/canary/admin/app.js +3 -0
- package/core/server/web/api/canary/admin/routes.js +3 -0
- package/core/server/web/api/canary/content/app.js +3 -0
- package/core/server/web/api/middleware/cors.js +1 -1
- package/core/server/web/api/v2/admin/app.js +3 -0
- package/core/server/web/api/v2/content/app.js +3 -0
- package/core/server/web/api/v3/admin/app.js +3 -0
- package/core/server/web/api/v3/content/app.js +3 -0
- package/core/server/web/members/app.js +5 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +4 -2
- package/core/shared/settings-cache/public.js +1 -1
- package/package.json +82 -78
- package/yarn.lock +1062 -679
- package/core/built/assets/ghost-dark-1933079797e24ccb8839657020830be5.css +0 -1
- package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +0 -1
- package/core/server/services/stats/lib/members-stats-service.js +0 -161
- 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;"> </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;"> </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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
60
|
-
<script src="assets/ghost.min-
|
|
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.
|
|
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-
|
|
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-
|
|
60
|
-
<script src="assets/ghost.min-
|
|
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');
|