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
|
@@ -187,9 +187,8 @@ function ampContent() {
|
|
|
187
187
|
// Use cheerio to traverse through HTML and make little clean-ups
|
|
188
188
|
$ = cheerio.load(ampHTML);
|
|
189
189
|
|
|
190
|
-
// We have to remove source children in video, as source
|
|
191
|
-
//
|
|
192
|
-
// errors in video, because video will be stripped out.
|
|
190
|
+
// We have to remove source children in video, as source is allowed for audio,
|
|
191
|
+
// but causes validation errors in video, because video will be stripped out.
|
|
193
192
|
// @TODO: remove this, when Amperize support video transform
|
|
194
193
|
$('video').children('source').remove();
|
|
195
194
|
$('video').children('track').remove();
|
|
@@ -987,7 +987,7 @@
|
|
|
987
987
|
<header class="page-header">
|
|
988
988
|
<a href="{{@site.url}}">
|
|
989
989
|
{{#if @site.icon}}
|
|
990
|
-
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed"></amp-img>
|
|
990
|
+
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed" alt="{{@site.title}}"></amp-img>
|
|
991
991
|
{{else}}
|
|
992
992
|
{{@site.title}}
|
|
993
993
|
{{/if}}
|
|
@@ -1006,7 +1006,9 @@
|
|
|
1006
1006
|
</header>
|
|
1007
1007
|
{{#if feature_image}}
|
|
1008
1008
|
<figure class="post-image">
|
|
1009
|
-
<amp-img src="{{img_url feature_image absolute="true"}}" width="600" height="340" layout="responsive"
|
|
1009
|
+
<amp-img src="{{img_url feature_image absolute="true"}}" width="600" height="340" layout="responsive"
|
|
1010
|
+
alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}"
|
|
1011
|
+
></amp-img>
|
|
1010
1012
|
</figure>
|
|
1011
1013
|
{{/if}}
|
|
1012
1014
|
<section class="post-content">
|
|
@@ -1020,7 +1022,7 @@
|
|
|
1020
1022
|
{{/post}}
|
|
1021
1023
|
<footer class="page-footer">
|
|
1022
1024
|
{{#if @site.icon}}
|
|
1023
|
-
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed"></amp-img>
|
|
1025
|
+
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed" alt="{{@site.title}}"></amp-img>
|
|
1024
1026
|
{{/if}}
|
|
1025
1027
|
<h3>{{@site.title}}</h3>
|
|
1026
1028
|
{{#if @site.description}}
|
|
@@ -80,7 +80,7 @@ function resolvePaths(globals, data, value) {
|
|
|
80
80
|
path = path.replace(/\.\[/g, '[');
|
|
81
81
|
|
|
82
82
|
if (path.charAt(0) === '@') {
|
|
83
|
-
result = jsonpath.query(globals, path.
|
|
83
|
+
result = jsonpath.query(globals, path.slice(1));
|
|
84
84
|
} else {
|
|
85
85
|
// Do the query, which always returns an array of matches
|
|
86
86
|
result = jsonpath.query(data, path);
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('services:routing:controllers:unsubscribe');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const url = require('url');
|
|
4
|
+
|
|
5
|
+
const urlUtils = require('../../../../shared/url-utils');
|
|
3
6
|
const megaService = require('../../../../server/services/mega');
|
|
4
7
|
const renderer = require('../../rendering');
|
|
8
|
+
const labs = require('../../../../shared/labs');
|
|
5
9
|
|
|
6
10
|
module.exports = async function unsubscribeController(req, res) {
|
|
7
11
|
debug('unsubscribeController');
|
|
8
12
|
|
|
13
|
+
if (labs.isSet('multipleNewslettersUI')) {
|
|
14
|
+
const {query} = url.parse(req.url, true);
|
|
15
|
+
|
|
16
|
+
if (!query || !query.uuid) {
|
|
17
|
+
res.writeHead(400);
|
|
18
|
+
return res.end('Email address not found.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const redirectUrl = new URL(urlUtils.urlFor('home', true));
|
|
22
|
+
redirectUrl.searchParams.append('uuid', query.uuid);
|
|
23
|
+
if (query.newsletter) {
|
|
24
|
+
redirectUrl.searchParams.append('newsletter', query.newsletter);
|
|
25
|
+
}
|
|
26
|
+
redirectUrl.searchParams.append('action', 'unsubscribe');
|
|
27
|
+
|
|
28
|
+
return res.redirect(302, redirectUrl.href);
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
let data = {};
|
|
10
32
|
|
|
11
33
|
try {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const {URL} = require('url');
|
|
2
|
+
const cors = require('cors');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
4
|
+
const config = require('../../../shared/config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dynamically configures the expressjs/cors middleware
|
|
8
|
+
*
|
|
9
|
+
* @param {import('express').Request} req
|
|
10
|
+
* @param {Function} callback
|
|
11
|
+
*/
|
|
12
|
+
function corsOptionsDelegate(req, callback) {
|
|
13
|
+
const origin = req.header('Origin');
|
|
14
|
+
const corsOptions = {
|
|
15
|
+
origin: false, // disallow cross-origin requests by default
|
|
16
|
+
credentials: true // required to allow admin-client to login to private sites
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (!origin || origin === 'null') {
|
|
20
|
+
return callback(null, corsOptions);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let originUrl;
|
|
24
|
+
try {
|
|
25
|
+
originUrl = new URL(origin);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return callback(new errors.BadRequestError({err}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// originUrl will definitely exist here because according to WHATWG URL spec
|
|
31
|
+
// The class constructor will either throw a TypeError or return a URL object
|
|
32
|
+
// https://url.spec.whatwg.org/#url-class
|
|
33
|
+
|
|
34
|
+
// allow all localhost and 127.0.0.1 requests no matter the port
|
|
35
|
+
if (originUrl.hostname === 'localhost' || originUrl.hostname === '127.0.0.1') {
|
|
36
|
+
corsOptions.origin = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// allow the configured host through on any protocol
|
|
40
|
+
const siteUrl = new URL(config.get('url'));
|
|
41
|
+
if (originUrl.host === siteUrl.host) {
|
|
42
|
+
corsOptions.origin = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// allow the configured admin:url host through on any protocol
|
|
46
|
+
if (config.get('admin:url')) {
|
|
47
|
+
const adminUrl = new URL(config.get('admin:url'));
|
|
48
|
+
if (originUrl.host === adminUrl.host) {
|
|
49
|
+
corsOptions.origin = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
callback(null, corsOptions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = cors(corsOptionsDelegate);
|
|
@@ -4,18 +4,18 @@ const constants = require('@tryghost/constants');
|
|
|
4
4
|
const themeEngine = require('../../services/theme-engine');
|
|
5
5
|
const express = require('../../../shared/express');
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
const
|
|
7
|
+
function isDeniedFile(file) {
|
|
8
|
+
const deniedFileTypes = ['.hbs', '.md', '.json'];
|
|
9
9
|
const ext = path.extname(file);
|
|
10
10
|
|
|
11
|
-
return
|
|
11
|
+
return deniedFileTypes.includes(ext);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
const
|
|
14
|
+
function isAllowedFile(file) {
|
|
15
|
+
const allowedFiles = ['manifest.json'];
|
|
16
16
|
const base = path.basename(file);
|
|
17
17
|
|
|
18
|
-
return
|
|
18
|
+
return allowedFiles.includes(base);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function forwardToExpressStatic(req, res, next) {
|
|
@@ -31,8 +31,8 @@ function forwardToExpressStatic(req, res, next) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function staticTheme() {
|
|
34
|
-
return function
|
|
35
|
-
if (!
|
|
34
|
+
return function denyStatic(req, res, next) {
|
|
35
|
+
if (!isAllowedFile(req.path) && isDeniedFile(req.path)) {
|
|
36
36
|
return next();
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('frontend');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const express = require('../../shared/express');
|
|
4
|
-
const cors = require('cors');
|
|
5
|
-
const {URL} = require('url');
|
|
6
|
-
const errors = require('@tryghost/errors');
|
|
7
4
|
const DomainEvents = require('@tryghost/domain-events');
|
|
8
5
|
const {MemberPageViewEvent} = require('@tryghost/member-events');
|
|
9
6
|
|
|
@@ -31,50 +28,6 @@ const STATIC_FILES_URL_PREFIX = `/${constants.STATIC_FILES_URL_PREFIX}`;
|
|
|
31
28
|
|
|
32
29
|
let router;
|
|
33
30
|
|
|
34
|
-
const corsOptionsDelegate = function corsOptionsDelegate(req, callback) {
|
|
35
|
-
const origin = req.header('Origin');
|
|
36
|
-
const corsOptions = {
|
|
37
|
-
origin: false, // disallow cross-origin requests by default
|
|
38
|
-
credentials: true // required to allow admin-client to login to private sites
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
if (!origin || origin === 'null') {
|
|
42
|
-
return callback(null, corsOptions);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let originUrl;
|
|
46
|
-
try {
|
|
47
|
-
originUrl = new URL(origin);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
return callback(new errors.BadRequestError({err}));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// originUrl will definitely exist here because according to WHATWG URL spec
|
|
53
|
-
// The class constructor will either throw a TypeError or return a URL object
|
|
54
|
-
// https://url.spec.whatwg.org/#url-class
|
|
55
|
-
|
|
56
|
-
// allow all localhost and 127.0.0.1 requests no matter the port
|
|
57
|
-
if (originUrl.hostname === 'localhost' || originUrl.hostname === '127.0.0.1') {
|
|
58
|
-
corsOptions.origin = true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// allow the configured host through on any protocol
|
|
62
|
-
const siteUrl = new URL(config.get('url'));
|
|
63
|
-
if (originUrl.host === siteUrl.host) {
|
|
64
|
-
corsOptions.origin = true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// allow the configured admin:url host through on any protocol
|
|
68
|
-
if (config.get('admin:url')) {
|
|
69
|
-
const adminUrl = new URL(config.get('admin:url'));
|
|
70
|
-
if (originUrl.host === adminUrl.host) {
|
|
71
|
-
corsOptions.origin = true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
callback(null, corsOptions);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
31
|
function SiteRouter(req, res, next) {
|
|
79
32
|
router(req, res, next);
|
|
80
33
|
}
|
|
@@ -89,7 +42,7 @@ module.exports = function setupSiteApp(options = {}) {
|
|
|
89
42
|
siteApp.set('view engine', 'hbs');
|
|
90
43
|
|
|
91
44
|
// enable CORS headers (allows admin client to hit front-end when configured on separate URLs)
|
|
92
|
-
siteApp.use(cors
|
|
45
|
+
siteApp.use(mw.cors);
|
|
93
46
|
|
|
94
47
|
siteApp.use(offersService.middleware);
|
|
95
48
|
|
|
@@ -51,14 +51,14 @@ module.exports = {
|
|
|
51
51
|
})
|
|
52
52
|
.then((data) => {
|
|
53
53
|
try {
|
|
54
|
-
return auth.setup.doFixtures(data
|
|
54
|
+
return auth.setup.doFixtures(data);
|
|
55
55
|
} catch (e) {
|
|
56
56
|
return data;
|
|
57
57
|
}
|
|
58
58
|
})
|
|
59
59
|
.then((data) => {
|
|
60
60
|
try {
|
|
61
|
-
return auth.setup.
|
|
61
|
+
return auth.setup.doProductAndNewsletter(data, api);
|
|
62
62
|
} catch (e) {
|
|
63
63
|
return data;
|
|
64
64
|
}
|
|
@@ -366,6 +366,9 @@ module.exports = {
|
|
|
366
366
|
if (labsService.isSet('multipleProducts')) {
|
|
367
367
|
frame.options.withRelated.push('products');
|
|
368
368
|
}
|
|
369
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
370
|
+
frame.options.withRelated.push('newsletters');
|
|
371
|
+
}
|
|
369
372
|
const page = await membersService.api.members.list(frame.options);
|
|
370
373
|
|
|
371
374
|
return page;
|
|
@@ -1,45 +1,127 @@
|
|
|
1
1
|
const models = require('../../models');
|
|
2
|
+
const tpl = require('@tryghost/tpl');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
4
|
+
const allowedIncludes = ['count.posts', 'count.members'];
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
newsletterNotFound: 'Newsletter not found.'
|
|
8
|
+
};
|
|
9
|
+
const newslettersService = require('../../services/newsletters');
|
|
2
10
|
|
|
3
11
|
module.exports = {
|
|
4
12
|
docName: 'newsletters',
|
|
5
13
|
|
|
6
14
|
browse: {
|
|
7
15
|
options: [
|
|
16
|
+
'include',
|
|
8
17
|
'filter',
|
|
9
18
|
'fields',
|
|
10
19
|
'limit',
|
|
11
20
|
'order',
|
|
12
21
|
'page'
|
|
13
22
|
],
|
|
23
|
+
validation: {
|
|
24
|
+
options: {
|
|
25
|
+
include: {
|
|
26
|
+
values: allowedIncludes
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
14
30
|
permissions: true,
|
|
15
31
|
query(frame) {
|
|
16
32
|
return models.Newsletter.findPage(frame.options);
|
|
17
33
|
}
|
|
18
34
|
},
|
|
19
35
|
|
|
36
|
+
read: {
|
|
37
|
+
options: [
|
|
38
|
+
'include',
|
|
39
|
+
'fields',
|
|
40
|
+
'debug',
|
|
41
|
+
// NOTE: only for internal context
|
|
42
|
+
'forUpdate',
|
|
43
|
+
'transacting'
|
|
44
|
+
],
|
|
45
|
+
data: [
|
|
46
|
+
'id',
|
|
47
|
+
'slug',
|
|
48
|
+
'uuid'
|
|
49
|
+
],
|
|
50
|
+
validation: {
|
|
51
|
+
options: {
|
|
52
|
+
include: {
|
|
53
|
+
values: allowedIncludes
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
permissions: true,
|
|
58
|
+
async query(frame) {
|
|
59
|
+
const newsletter = models.Newsletter.findOne(frame.data, frame.options);
|
|
60
|
+
|
|
61
|
+
if (!newsletter) {
|
|
62
|
+
throw new errors.NotFoundError({
|
|
63
|
+
message: tpl(messages.newsletterNotFound)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return newsletter;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
20
70
|
add: {
|
|
21
71
|
statusCode: 201,
|
|
72
|
+
headers: {
|
|
73
|
+
cacheInvalidate: true
|
|
74
|
+
},
|
|
75
|
+
options: [
|
|
76
|
+
'include',
|
|
77
|
+
'opt_in_existing'
|
|
78
|
+
],
|
|
79
|
+
validation: {
|
|
80
|
+
options: {
|
|
81
|
+
include: {
|
|
82
|
+
values: allowedIncludes
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
22
86
|
permissions: true,
|
|
23
87
|
async query(frame) {
|
|
24
|
-
return
|
|
88
|
+
return newslettersService.add(frame.data.newsletters[0], frame.options);
|
|
25
89
|
}
|
|
26
90
|
},
|
|
27
91
|
|
|
28
92
|
edit: {
|
|
29
|
-
headers: {
|
|
93
|
+
headers: {
|
|
94
|
+
cacheInvalidate: true
|
|
95
|
+
},
|
|
30
96
|
options: [
|
|
31
|
-
'id'
|
|
97
|
+
'id',
|
|
98
|
+
'include'
|
|
32
99
|
],
|
|
33
100
|
validation: {
|
|
34
101
|
options: {
|
|
35
102
|
id: {
|
|
36
103
|
required: true
|
|
104
|
+
},
|
|
105
|
+
include: {
|
|
106
|
+
values: allowedIncludes
|
|
37
107
|
}
|
|
38
108
|
}
|
|
39
109
|
},
|
|
40
110
|
permissions: true,
|
|
41
111
|
async query(frame) {
|
|
42
|
-
return
|
|
112
|
+
return newslettersService.edit(frame.data.newsletters[0], frame.options);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
verifyPropertyUpdate: {
|
|
117
|
+
permissions: {
|
|
118
|
+
method: 'edit'
|
|
119
|
+
},
|
|
120
|
+
data: [
|
|
121
|
+
'token'
|
|
122
|
+
],
|
|
123
|
+
async query(frame) {
|
|
124
|
+
return newslettersService.verifyPropertyUpdate(frame.data.token);
|
|
43
125
|
}
|
|
44
126
|
}
|
|
45
127
|
};
|
|
@@ -8,7 +8,7 @@ module.exports = {
|
|
|
8
8
|
method: 'browse'
|
|
9
9
|
},
|
|
10
10
|
async query() {
|
|
11
|
-
return await statsService.
|
|
11
|
+
return await statsService.getMemberCountHistory();
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
mrr: {
|
|
@@ -17,7 +17,16 @@ module.exports = {
|
|
|
17
17
|
method: 'browse'
|
|
18
18
|
},
|
|
19
19
|
async query() {
|
|
20
|
-
return await statsService.
|
|
20
|
+
return await statsService.getMRRHistory();
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
subscriptions: {
|
|
24
|
+
permissions: {
|
|
25
|
+
docName: 'members',
|
|
26
|
+
method: 'browse'
|
|
27
|
+
},
|
|
28
|
+
async query() {
|
|
29
|
+
return await statsService.getSubscriptionCountHistory();
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('api:canary:utils:serializers:input:members');
|
|
3
|
+
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
4
|
+
const labsService = require('../../../../../../shared/labs');
|
|
3
5
|
|
|
4
6
|
function defaultRelations(frame) {
|
|
5
7
|
if (frame.options.withRelated) {
|
|
@@ -17,6 +19,26 @@ module.exports = {
|
|
|
17
19
|
browse(apiConfig, frame) {
|
|
18
20
|
debug('browse');
|
|
19
21
|
defaultRelations(frame);
|
|
22
|
+
|
|
23
|
+
if (!frame.options.order) {
|
|
24
|
+
frame.options.autoOrder = 'created_at DESC, id DESC';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
28
|
+
frame.options.mongoTransformer = mapNQLKeyValues({
|
|
29
|
+
key: {
|
|
30
|
+
from: 'subscribed',
|
|
31
|
+
to: 'newsletters.status'
|
|
32
|
+
},
|
|
33
|
+
values: [{
|
|
34
|
+
from: true,
|
|
35
|
+
to: 'active'
|
|
36
|
+
}, {
|
|
37
|
+
from: false,
|
|
38
|
+
to: {$ne: 'active'}
|
|
39
|
+
}]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
20
42
|
},
|
|
21
43
|
|
|
22
44
|
read() {
|
|
@@ -132,8 +132,19 @@ function serializeMember(member, options) {
|
|
|
132
132
|
serialized.products = json.products;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
135
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
136
|
+
if (json.newsletters) {
|
|
137
|
+
serialized.newsletters = json.newsletters
|
|
138
|
+
.filter(newsletter => newsletter.status === 'active')
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
return a.sort_order - b.sort_order;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// override the `subscribed` param to mean "subscribed to any active newsletter"
|
|
144
|
+
serialized.subscribed = false;
|
|
145
|
+
if (serialized.newsletters.length > 0) {
|
|
146
|
+
serialized.subscribed = true;
|
|
147
|
+
}
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
return serialized;
|
|
@@ -83,7 +83,7 @@ const http = (apiImpl) => {
|
|
|
83
83
|
|
|
84
84
|
// CASE: generate headers based on the api ctrl configuration
|
|
85
85
|
if (req && req.headers && req.headers['accept-version'] && res.locals) {
|
|
86
|
-
headers['
|
|
86
|
+
headers['Content-Version'] = `v${res.locals.safeVersion}`;
|
|
87
87
|
}
|
|
88
88
|
res.set(headers);
|
|
89
89
|
|
|
@@ -45,6 +45,8 @@ const mapPost = (model, frame) => {
|
|
|
45
45
|
}
|
|
46
46
|
date.forPost(jsonModel);
|
|
47
47
|
gating.forPost(jsonModel, frame);
|
|
48
|
+
|
|
49
|
+
delete jsonModel.newsletter_id;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
if (typeof jsonModel.email_recipient_filter === 'undefined') {
|
|
@@ -100,6 +102,7 @@ const mapPage = (model, frame) => {
|
|
|
100
102
|
delete jsonModel.email_subject;
|
|
101
103
|
delete jsonModel.send_email_when_published;
|
|
102
104
|
delete jsonModel.email_recipient_filter;
|
|
105
|
+
delete jsonModel.newsletter_id;
|
|
103
106
|
|
|
104
107
|
return jsonModel;
|
|
105
108
|
};
|
|
@@ -440,6 +440,44 @@ function createDropColumnMigration(table, column, columnDefinition) {
|
|
|
440
440
|
);
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
/**
|
|
444
|
+
* @param {string} table
|
|
445
|
+
* @param {string} column
|
|
446
|
+
*
|
|
447
|
+
* @returns {Migration}
|
|
448
|
+
*/
|
|
449
|
+
function createSetNullableMigration(table, column) {
|
|
450
|
+
return createNonTransactionalMigration(
|
|
451
|
+
async function up(knex) {
|
|
452
|
+
logging.info(`Setting nullable: ${table}.${column}`);
|
|
453
|
+
await commands.setNullable(table, column, knex);
|
|
454
|
+
},
|
|
455
|
+
async function down(knex) {
|
|
456
|
+
logging.info(`Dropping nullable: ${table}.${column}`);
|
|
457
|
+
await commands.dropNullable(table, column, knex);
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @param {string} table
|
|
464
|
+
* @param {string} column
|
|
465
|
+
*
|
|
466
|
+
* @returns {Migration}
|
|
467
|
+
*/
|
|
468
|
+
function createDropNullableMigration(table, column) {
|
|
469
|
+
return createNonTransactionalMigration(
|
|
470
|
+
async function up(knex) {
|
|
471
|
+
logging.info(`Dropping nullable: ${table}.${column}`);
|
|
472
|
+
await commands.dropNullable(table, column, knex);
|
|
473
|
+
},
|
|
474
|
+
async function down(knex) {
|
|
475
|
+
logging.info(`Setting nullable: ${table}.${column}`);
|
|
476
|
+
await commands.setNullable(table, column, knex);
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
443
481
|
/**
|
|
444
482
|
* Creates a migration which will insert a new setting in settings table
|
|
445
483
|
* @param {object} settingSpec - setting key, value, group and type
|
|
@@ -505,6 +543,8 @@ module.exports = {
|
|
|
505
543
|
combineNonTransactionalMigrations,
|
|
506
544
|
createAddColumnMigration,
|
|
507
545
|
createDropColumnMigration,
|
|
546
|
+
createSetNullableMigration,
|
|
547
|
+
createDropNullableMigration,
|
|
508
548
|
meta: {
|
|
509
549
|
MIGRATION_USER
|
|
510
550
|
}
|
package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js
CHANGED
|
@@ -7,8 +7,8 @@ module.exports = recreateTable('newsletters', {
|
|
|
7
7
|
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
8
8
|
sender_name: {type: 'string', maxlength: 191, nullable: false},
|
|
9
9
|
sender_email: {type: 'string', maxlength: 191, nullable: true},
|
|
10
|
-
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: ['newsletter', 'support']}},
|
|
11
|
-
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
|
|
10
|
+
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: [['newsletter', 'support']]}},
|
|
11
|
+
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active', validations: {isIn: [['active', 'archived']]}},
|
|
12
12
|
visibility: {
|
|
13
13
|
type: 'string',
|
|
14
14
|
maxlength: 50,
|
|
@@ -20,10 +20,10 @@ module.exports = recreateTable('newsletters', {
|
|
|
20
20
|
header_image: {type: 'string', maxlength: 2000, nullable: true},
|
|
21
21
|
show_header_icon: {type: 'bool', nullable: false, defaultTo: true},
|
|
22
22
|
show_header_title: {type: 'bool', nullable: false, defaultTo: true},
|
|
23
|
-
title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
|
|
24
|
-
title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center', validations: {isIn: ['center', 'left']}},
|
|
23
|
+
title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: [['serif', 'sans_serif']]}},
|
|
24
|
+
title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center', validations: {isIn: [['center', 'left']]}},
|
|
25
25
|
show_feature_image: {type: 'bool', nullable: false, defaultTo: true},
|
|
26
|
-
body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
|
|
26
|
+
body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: [['serif', 'sans_serif']]}},
|
|
27
27
|
footer_content: {type: 'text', maxlength: 1000000000, nullable: true},
|
|
28
28
|
show_badge: {type: 'bool', nullable: false, defaultTo: true}
|
|
29
29
|
});
|