ghost 5.26.4 → 5.28.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/components/tryghost-adapter-manager-5.28.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.26.4.tgz → tryghost-api-framework-5.28.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.26.4.tgz → tryghost-api-version-compatibility-service-5.28.0.tgz} +0 -0
- package/components/tryghost-audience-feedback-5.28.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.28.0.tgz +0 -0
- package/components/tryghost-constants-5.28.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.26.4.tgz → tryghost-custom-theme-settings-service-5.28.0.tgz} +0 -0
- package/components/tryghost-data-generator-5.28.0.tgz +0 -0
- package/components/tryghost-domain-events-5.28.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.28.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.28.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.28.0.tgz +0 -0
- package/components/tryghost-email-events-5.28.0.tgz +0 -0
- package/components/tryghost-email-service-5.28.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.28.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.28.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.28.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.28.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.28.0.tgz +0 -0
- package/components/tryghost-job-manager-5.28.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.28.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.28.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.28.0.tgz +0 -0
- package/components/tryghost-magic-link-5.28.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.28.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.26.4.tgz → tryghost-member-attribution-5.28.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.26.4.tgz → tryghost-member-events-5.28.0.tgz} +0 -0
- package/components/{tryghost-members-api-5.26.4.tgz → tryghost-members-api-5.28.0.tgz} +0 -0
- package/components/tryghost-members-csv-5.28.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.28.0.tgz +0 -0
- package/components/tryghost-members-importer-5.28.0.tgz +0 -0
- package/components/tryghost-members-offers-5.28.0.tgz +0 -0
- package/components/tryghost-members-payments-5.28.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.28.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.28.0.tgz +0 -0
- package/components/tryghost-minifier-5.28.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.28.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.28.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.28.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.28.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.28.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.28.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.28.0.tgz +0 -0
- package/components/tryghost-package-json-5.28.0.tgz +0 -0
- package/components/tryghost-referrers-5.28.0.tgz +0 -0
- package/components/tryghost-security-5.28.0.tgz +0 -0
- package/components/tryghost-session-service-5.28.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.28.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.26.4.tgz → tryghost-staff-service-5.28.0.tgz} +0 -0
- package/components/tryghost-stats-service-5.28.0.tgz +0 -0
- package/components/tryghost-tiers-5.28.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.28.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.28.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.28.0.tgz +0 -0
- package/core/built/admin/assets/{chunk.143.f9454a947ce3ca9468a9.js → chunk.143.16c07be45a0c39262995.js} +6 -6
- package/core/built/admin/assets/{chunk.178.5730ec1241c554356843.js → chunk.178.93714202b2fc31b2b86a.js} +4 -4
- package/core/built/admin/assets/{chunk.613.25718fa355a25fc2a7c1.js → chunk.380.de3a9bf5161ab5300d23.js} +4790 -4306
- package/core/built/admin/assets/{chunk.613.25718fa355a25fc2a7c1.js.LICENSE.txt → chunk.380.de3a9bf5161ab5300d23.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/ghost-1532517fd24b662b42a85ea0881865a5.css +1 -0
- package/core/built/admin/assets/{ghost-5429d972966c85a0a24d766f93db999d.js → ghost-d8834e309d8e4800057d6d17fed9415b.js} +4311 -4289
- package/core/built/admin/assets/ghost-dark-3b8aff3cf9db67f6ad59abec25ae98be.css +1 -0
- package/core/built/admin/assets/{vendor-5aae14724f891e36c43786254980485c.js → vendor-dbf84caa0968ef5da0486055da6636da.js} +113 -63
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/get.js +2 -2
- package/core/frontend/web/middleware/error-handler.js +0 -2
- package/core/server/api/endpoints/themes.js +13 -9
- package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +2 -0
- package/core/server/data/migrations/versions/5.27/2022-12-13-16-15-add-usage-colums-to-tokens.js +20 -0
- package/core/server/data/migrations/versions/5.27/2023-01-04-04-12-drop-suppressions-table.js +14 -0
- package/core/server/data/migrations/versions/5.27/2023-01-04-04-13-add-suppressions-table.js +9 -0
- package/core/server/data/migrations/versions/5.28/2023-01-05-15-13-add-active-theme-permissions.js +12 -0
- package/core/server/data/schema/fixtures/fixtures.json +7 -2
- package/core/server/data/schema/schema.js +4 -1
- package/core/server/models/member.js +2 -1
- package/core/server/models/product.js +1 -1
- package/core/server/models/single-use-token.js +2 -25
- package/core/server/services/bulk-email/bulk-email-processor.js +23 -4
- package/core/server/services/email-service/wrapper.js +2 -1
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +13 -5
- package/core/server/services/mega/mega.js +9 -1
- package/core/server/services/members/SingleUseTokenProvider.js +51 -7
- package/core/server/services/members/api.js +8 -1
- package/core/server/services/members/jobs/clean-tokens.js +50 -0
- package/core/server/services/members/jobs/index.js +29 -4
- package/core/server/services/members/service.js +12 -19
- package/core/server/services/newsletters/index.js +8 -1
- package/core/server/services/settings/settings-service.js +8 -1
- package/core/server/services/themes/activate.js +3 -3
- package/core/server/services/themes/index.js +4 -2
- package/core/server/services/themes/storage.js +3 -1
- package/core/server/services/themes/to-json.js +6 -6
- package/core/server/services/themes/validate.js +80 -8
- package/core/server/web/api/endpoints/admin/routes.js +5 -0
- package/core/shared/config/defaults.json +3 -2
- package/core/shared/labs.js +2 -1
- package/package.json +133 -133
- package/yarn.lock +1840 -2000
- package/components/tryghost-adapter-manager-5.26.4.tgz +0 -0
- package/components/tryghost-audience-feedback-5.26.4.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.26.4.tgz +0 -0
- package/components/tryghost-constants-5.26.4.tgz +0 -0
- package/components/tryghost-data-generator-5.26.4.tgz +0 -0
- package/components/tryghost-domain-events-5.26.4.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.26.4.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.26.4.tgz +0 -0
- package/components/tryghost-email-content-generator-5.26.4.tgz +0 -0
- package/components/tryghost-email-events-5.26.4.tgz +0 -0
- package/components/tryghost-email-service-5.26.4.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.26.4.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.26.4.tgz +0 -0
- package/components/tryghost-extract-api-key-5.26.4.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.26.4.tgz +0 -0
- package/components/tryghost-importer-revue-5.26.4.tgz +0 -0
- package/components/tryghost-job-manager-5.26.4.tgz +0 -0
- package/components/tryghost-link-redirects-5.26.4.tgz +0 -0
- package/components/tryghost-link-replacer-5.26.4.tgz +0 -0
- package/components/tryghost-link-tracking-5.26.4.tgz +0 -0
- package/components/tryghost-magic-link-5.26.4.tgz +0 -0
- package/components/tryghost-mailgun-client-5.26.4.tgz +0 -0
- package/components/tryghost-members-csv-5.26.4.tgz +0 -0
- package/components/tryghost-members-events-service-5.26.4.tgz +0 -0
- package/components/tryghost-members-importer-5.26.4.tgz +0 -0
- package/components/tryghost-members-offers-5.26.4.tgz +0 -0
- package/components/tryghost-members-payments-5.26.4.tgz +0 -0
- package/components/tryghost-members-ssr-5.26.4.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.26.4.tgz +0 -0
- package/components/tryghost-minifier-5.26.4.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.26.4.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.26.4.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.26.4.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.26.4.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.26.4.tgz +0 -0
- package/components/tryghost-mw-vhost-5.26.4.tgz +0 -0
- package/components/tryghost-oembed-service-5.26.4.tgz +0 -0
- package/components/tryghost-package-json-5.26.4.tgz +0 -0
- package/components/tryghost-referrers-5.26.4.tgz +0 -0
- package/components/tryghost-security-5.26.4.tgz +0 -0
- package/components/tryghost-session-service-5.26.4.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.26.4.tgz +0 -0
- package/components/tryghost-stats-service-5.26.4.tgz +0 -0
- package/components/tryghost-tiers-5.26.4.tgz +0 -0
- package/components/tryghost-update-check-service-5.26.4.tgz +0 -0
- package/components/tryghost-verification-trigger-5.26.4.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.26.4.tgz +0 -0
- package/core/built/admin/assets/ghost-830670d910cea64d98ab083aafd1dffd.css +0 -1
- package/core/built/admin/assets/ghost-dark-e9195020925a360d83163e1e87b514a6.css +0 -1
|
@@ -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%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.
|
|
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%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.28%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%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" />
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</style>
|
|
38
38
|
|
|
39
39
|
<link integrity="" rel="stylesheet" href="assets/vendor-3e6947aa681f0fb82b193090e520dc73.css">
|
|
40
|
-
<link integrity="" rel="stylesheet" href="assets/ghost-
|
|
40
|
+
<link integrity="" rel="stylesheet" href="assets/ghost-1532517fd24b662b42a85ea0881865a5.css" title="light">
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
</head>
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
|
|
57
57
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
58
58
|
|
|
59
|
-
<script src="assets/vendor-
|
|
60
|
-
<script src="assets/chunk.
|
|
61
|
-
<script src="assets/chunk.143.
|
|
62
|
-
<script src="assets/ghost-
|
|
59
|
+
<script src="assets/vendor-dbf84caa0968ef5da0486055da6636da.js"></script>
|
|
60
|
+
<script src="assets/chunk.380.de3a9bf5161ab5300d23.js"></script>
|
|
61
|
+
<script src="assets/chunk.143.16c07be45a0c39262995.js"></script>
|
|
62
|
+
<script src="assets/ghost-d8834e309d8e4800057d6d17fed9415b.js"></script>
|
|
63
63
|
</body>
|
|
64
64
|
</html>
|
|
@@ -13,7 +13,7 @@ const jsonpath = require('jsonpath');
|
|
|
13
13
|
|
|
14
14
|
const messages = {
|
|
15
15
|
mustBeCalledAsBlock: 'The {\\{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}',
|
|
16
|
-
invalidResource: 'Invalid resource given to get helper'
|
|
16
|
+
invalidResource: 'Invalid "{resource}" resource given to get helper'
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const createFrame = hbs.handlebars.createFrame;
|
|
@@ -193,7 +193,7 @@ module.exports = async function get(resource, options) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
if (!RESOURCES[resource]) {
|
|
196
|
-
data.error = tpl(messages.invalidResource);
|
|
196
|
+
data.error = tpl(messages.invalidResource, {resource});
|
|
197
197
|
logging.warn(data.error);
|
|
198
198
|
return options.inverse(self, {data: data});
|
|
199
199
|
}
|
|
@@ -4,6 +4,7 @@ const models = require('../../models');
|
|
|
4
4
|
|
|
5
5
|
// Used to emit theme.uploaded which is used in core/server/analytics-events
|
|
6
6
|
const events = require('../../lib/common/events');
|
|
7
|
+
const {settingsCache} = require('../../services/settings-helpers');
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
docName: 'themes',
|
|
@@ -15,6 +16,15 @@ module.exports = {
|
|
|
15
16
|
}
|
|
16
17
|
},
|
|
17
18
|
|
|
19
|
+
readActive: {
|
|
20
|
+
permissions: true,
|
|
21
|
+
async query() {
|
|
22
|
+
let themeName = settingsCache.get('active_theme');
|
|
23
|
+
const themeErrors = await themeService.api.getThemeErrors(themeName);
|
|
24
|
+
return themeService.api.getJSON(themeName, themeErrors);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
18
28
|
activate: {
|
|
19
29
|
headers: {
|
|
20
30
|
cacheInvalidate: true
|
|
@@ -42,15 +52,9 @@ module.exports = {
|
|
|
42
52
|
value: themeName
|
|
43
53
|
}];
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return models.Settings.edit(newSettings, frame.options)
|
|
49
|
-
.then(() => checkedTheme);
|
|
50
|
-
})
|
|
51
|
-
.then((checkedTheme) => {
|
|
52
|
-
return themeService.api.getJSON(themeName, checkedTheme);
|
|
53
|
-
});
|
|
55
|
+
const themeErrors = await themeService.api.activate(themeName);
|
|
56
|
+
await models.Settings.edit(newSettings, frame.options);
|
|
57
|
+
return themeService.api.getJSON(themeName, themeErrors);
|
|
54
58
|
}
|
|
55
59
|
},
|
|
56
60
|
|
|
@@ -4,6 +4,8 @@ module.exports = {
|
|
|
4
4
|
authors: require('./authors'),
|
|
5
5
|
comments: require('./comments'),
|
|
6
6
|
emails: require('./emails'),
|
|
7
|
+
emailBatches: require('./email-batches'),
|
|
8
|
+
emailFailures: require('./email-failures'),
|
|
7
9
|
images: require('./images'),
|
|
8
10
|
integrations: require('./integrations'),
|
|
9
11
|
pages: require('./pages'),
|
package/core/server/data/migrations/versions/5.27/2022-12-13-16-15-add-usage-colums-to-tokens.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = combineNonTransactionalMigrations(
|
|
4
|
+
createAddColumnMigration('tokens', 'updated_at', {
|
|
5
|
+
type: 'dateTime',
|
|
6
|
+
nullable: true
|
|
7
|
+
}),
|
|
8
|
+
|
|
9
|
+
createAddColumnMigration('tokens', 'first_used_at', {
|
|
10
|
+
type: 'dateTime',
|
|
11
|
+
nullable: true
|
|
12
|
+
}),
|
|
13
|
+
|
|
14
|
+
createAddColumnMigration('tokens', 'used_count', {
|
|
15
|
+
type: 'integer',
|
|
16
|
+
nullable: false,
|
|
17
|
+
unsigned: true,
|
|
18
|
+
defaultTo: 0
|
|
19
|
+
})
|
|
20
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
const migration = addTable('suppressions', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
email_address: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
6
|
+
email_id: {type: 'string', maxlength: 24, nullable: true, references: 'emails.id'},
|
|
7
|
+
reason: {type: 'string', maxlength: 50, nullable: false},
|
|
8
|
+
created_at: {type: 'dateTime', nullable: false}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
up: migration.down,
|
|
13
|
+
down: migration.up
|
|
14
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('suppressions', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
email: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
6
|
+
email_id: {type: 'string', maxlength: 24, nullable: true, references: 'emails.id'},
|
|
7
|
+
reason: {type: 'string', maxlength: 50, nullable: false},
|
|
8
|
+
created_at: {type: 'dateTime', nullable: false}
|
|
9
|
+
});
|
package/core/server/data/migrations/versions/5.28/2023-01-05-15-13-add-active-theme-permissions.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const {addPermissionWithRoles} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addPermissionWithRoles({
|
|
4
|
+
name: 'View active theme details',
|
|
5
|
+
action: 'readActive',
|
|
6
|
+
object: 'theme'
|
|
7
|
+
}, [
|
|
8
|
+
'Author',
|
|
9
|
+
'Editor',
|
|
10
|
+
'Administrator',
|
|
11
|
+
'Admin Integration'
|
|
12
|
+
]);
|
|
@@ -214,6 +214,11 @@
|
|
|
214
214
|
"action_type": "activate",
|
|
215
215
|
"object_type": "theme"
|
|
216
216
|
},
|
|
217
|
+
{
|
|
218
|
+
"name": "View active theme details",
|
|
219
|
+
"action_type": "readActive",
|
|
220
|
+
"object_type": "theme"
|
|
221
|
+
},
|
|
217
222
|
{
|
|
218
223
|
"name": "Upload themes",
|
|
219
224
|
"action_type": "add",
|
|
@@ -804,7 +809,7 @@
|
|
|
804
809
|
"user": "all",
|
|
805
810
|
"role": "all",
|
|
806
811
|
"invite": "all",
|
|
807
|
-
"theme": ["browse"],
|
|
812
|
+
"theme": ["browse", "readActive"],
|
|
808
813
|
"email_preview": "all",
|
|
809
814
|
"email": "all",
|
|
810
815
|
"snippet": "all",
|
|
@@ -819,7 +824,7 @@
|
|
|
819
824
|
"tag": ["browse", "read", "add"],
|
|
820
825
|
"user": ["browse", "read"],
|
|
821
826
|
"role": ["browse"],
|
|
822
|
-
"theme": ["browse"],
|
|
827
|
+
"theme": ["browse", "readActive"],
|
|
823
828
|
"email_preview": "read",
|
|
824
829
|
"email": "read",
|
|
825
830
|
"snippet": ["browse", "read"],
|
|
@@ -858,6 +858,9 @@ module.exports = {
|
|
|
858
858
|
token: {type: 'string', maxlength: 32, nullable: false, index: true},
|
|
859
859
|
data: {type: 'string', maxlength: 2000, nullable: true},
|
|
860
860
|
created_at: {type: 'dateTime', nullable: false},
|
|
861
|
+
updated_at: {type: 'dateTime', nullable: true},
|
|
862
|
+
first_used_at: {type: 'dateTime', nullable: true},
|
|
863
|
+
used_count: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
|
|
861
864
|
created_by: {type: 'string', maxlength: 24, nullable: false}
|
|
862
865
|
},
|
|
863
866
|
snippets: {
|
|
@@ -952,7 +955,7 @@ module.exports = {
|
|
|
952
955
|
},
|
|
953
956
|
suppressions: {
|
|
954
957
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
955
|
-
|
|
958
|
+
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
|
|
956
959
|
email_id: {type: 'string', maxlength: 24, nullable: true, references: 'emails.id'},
|
|
957
960
|
reason: {
|
|
958
961
|
type: 'string',
|
|
@@ -50,7 +50,7 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
50
50
|
replacement: 'emails.post_id',
|
|
51
51
|
// Currently we cannot expand on values such as null or a string in mongo-knex
|
|
52
52
|
// But the line below is essentially the same as: `email_recipients.opened_at:-null`
|
|
53
|
-
expansion: 'email_recipients.opened_at:>=0'
|
|
53
|
+
expansion: 'email_recipients.opened_at:>=0'
|
|
54
54
|
}];
|
|
55
55
|
},
|
|
56
56
|
|
|
@@ -165,6 +165,7 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
165
165
|
|
|
166
166
|
newsletters() {
|
|
167
167
|
return this.belongsToMany('Newsletter', 'members_newsletters', 'member_id', 'newsletter_id')
|
|
168
|
+
.query('orderBy', 'newsletters.sort_order', 'ASC')
|
|
168
169
|
.query((qb) => {
|
|
169
170
|
// avoids bookshelf adding a `DISTINCT` to the query
|
|
170
171
|
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
|
-
const logging = require('@tryghost/logging');
|
|
4
3
|
|
|
5
4
|
const SingleUseToken = ghostBookshelf.Model.extend({
|
|
6
5
|
tableName: 'tokens',
|
|
7
6
|
|
|
8
7
|
defaults() {
|
|
9
8
|
return {
|
|
9
|
+
used_count: 0,
|
|
10
10
|
token: crypto
|
|
11
11
|
.randomBytes(192 / 8)
|
|
12
12
|
.toString('base64')
|
|
@@ -15,30 +15,7 @@ const SingleUseToken = ghostBookshelf.Model.extend({
|
|
|
15
15
|
.replace(/\//g, '_')
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
}, {
|
|
19
|
-
async findOne(data, unfilteredOptions = {}) {
|
|
20
|
-
const model = await ghostBookshelf.Model.findOne.call(this, data, unfilteredOptions);
|
|
21
|
-
|
|
22
|
-
if (model) {
|
|
23
|
-
setTimeout(async () => {
|
|
24
|
-
try {
|
|
25
|
-
await this.destroy(Object.assign({
|
|
26
|
-
destroyBy: {
|
|
27
|
-
id: model.id
|
|
28
|
-
}
|
|
29
|
-
}, {
|
|
30
|
-
...unfilteredOptions,
|
|
31
|
-
transacting: null
|
|
32
|
-
}));
|
|
33
|
-
} catch (err) {
|
|
34
|
-
logging.error(err);
|
|
35
|
-
}
|
|
36
|
-
}, 10 * 60 * 1000);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return model;
|
|
40
|
-
}
|
|
41
|
-
});
|
|
18
|
+
}, {});
|
|
42
19
|
|
|
43
20
|
const SingleUseTokens = ghostBookshelf.Collection.extend({
|
|
44
21
|
model: SingleUseToken
|
|
@@ -12,6 +12,12 @@ const postEmailSerializer = require('../mega/post-email-serializer');
|
|
|
12
12
|
const configService = require('../../../shared/config');
|
|
13
13
|
const settingsCache = require('../../../shared/settings-cache');
|
|
14
14
|
|
|
15
|
+
async function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
setTimeout(resolve, ms);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
const messages = {
|
|
16
22
|
error: 'The email service received an error from mailgun and was unable to send.'
|
|
17
23
|
};
|
|
@@ -145,14 +151,27 @@ module.exports = {
|
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
// get recipient rows via knex to avoid costly bookshelf model instantiation
|
|
149
|
-
const recipientRows = await models.EmailRecipient
|
|
150
|
-
.getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`});
|
|
151
|
-
|
|
152
154
|
// Patch to prevent saving the related email model
|
|
153
155
|
await emailBatchModel.save({status: 'submitting'}, {...knexOptions, patch: true});
|
|
154
156
|
|
|
155
157
|
try {
|
|
158
|
+
// get recipient rows via knex to avoid costly bookshelf model instantiation
|
|
159
|
+
let recipientRows = await models.EmailRecipient.getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`}, knexOptions);
|
|
160
|
+
|
|
161
|
+
// For an unknown reason, the returned recipient rows is sometimes an empty array
|
|
162
|
+
// refs https://github.com/TryGhost/Team/issues/2246
|
|
163
|
+
let counter = 0;
|
|
164
|
+
while (recipientRows.length === 0 && counter < 5) {
|
|
165
|
+
logging.info('[sendEmailJob] Found zero recipients [retries:' + counter + '] for email batch ' + emailBatchId);
|
|
166
|
+
|
|
167
|
+
counter += 1;
|
|
168
|
+
await sleep(200);
|
|
169
|
+
recipientRows = await models.EmailRecipient.getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`}, knexOptions);
|
|
170
|
+
}
|
|
171
|
+
if (counter > 0) {
|
|
172
|
+
logging.info('[sendEmailJob] Recovered recipients [retries:' + counter + '] for email batch ' + emailBatchId + ' - ' + recipientRows.length + ' recipients found');
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
// Load newsletter data on email
|
|
157
176
|
await emailBatchModel.relations.email.getLazyRelation('newsletter', {require: false, ...knexOptions});
|
|
158
177
|
|
|
@@ -101,7 +101,8 @@ class EmailServiceWrapper {
|
|
|
101
101
|
emailRenderer,
|
|
102
102
|
emailSegmenter,
|
|
103
103
|
limitService,
|
|
104
|
-
membersRepository
|
|
104
|
+
membersRepository,
|
|
105
|
+
verificationTrigger: membersService.verificationTrigger
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
this.controller = new EmailController(this.service, {
|
|
@@ -35,7 +35,7 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
|
35
35
|
try {
|
|
36
36
|
await this.Suppression.destroy({
|
|
37
37
|
destroyBy: {
|
|
38
|
-
|
|
38
|
+
email: email
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
} catch (err) {
|
|
@@ -49,7 +49,7 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
|
49
49
|
async getSuppressionData(email) {
|
|
50
50
|
try {
|
|
51
51
|
const model = await this.Suppression.findOne({
|
|
52
|
-
|
|
52
|
+
email: email
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
if (!model) {
|
|
@@ -73,11 +73,11 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
75
|
const collection = await this.Suppression.findAll({
|
|
76
|
-
filter: `
|
|
76
|
+
filter: `email:[${emails.join(',')}]`
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
return emails.map((email) => {
|
|
80
|
-
const model = collection.models.find(m => m.get('
|
|
80
|
+
const model = collection.models.find(m => m.get('email') === email);
|
|
81
81
|
|
|
82
82
|
if (!model) {
|
|
83
83
|
return new EmailSuppressionData(false);
|
|
@@ -97,8 +97,16 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
|
97
97
|
async init() {
|
|
98
98
|
const handleEvent = reason => async (event) => {
|
|
99
99
|
try {
|
|
100
|
+
if (reason === 'bounce') {
|
|
101
|
+
if (!Number.isInteger(event.error?.code)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (event.error.code < 500 || event.error.code > 599 && event.error.code !== 605) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
100
108
|
await this.Suppression.add({
|
|
101
|
-
|
|
109
|
+
email: event.email,
|
|
102
110
|
email_id: event.emailId,
|
|
103
111
|
reason: reason,
|
|
104
112
|
created_at: event.timestamp
|
|
@@ -185,7 +185,7 @@ const addEmail = async (postModel, options) => {
|
|
|
185
185
|
await limitService.errorIfWouldGoOverLimit('emails');
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
if (
|
|
188
|
+
if (await membersService.verificationTrigger.checkVerificationRequired()) {
|
|
189
189
|
throw new errors.HostLimitError({
|
|
190
190
|
message: tpl(messages.emailSendingDisabled)
|
|
191
191
|
});
|
|
@@ -312,6 +312,14 @@ async function sendEmailJob({emailId, options}) {
|
|
|
312
312
|
await limitService.errorIfWouldGoOverLimit('emails');
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// Check email verification required
|
|
316
|
+
// We need to check this inside the job again
|
|
317
|
+
if (await membersService.verificationTrigger.checkVerificationRequired()) {
|
|
318
|
+
throw new errors.HostLimitError({
|
|
319
|
+
message: tpl(messages.emailSendingDisabled)
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
315
323
|
// Check if the email is still pending. And set the status to submitting in one transaction.
|
|
316
324
|
let hasSingleAccess = false;
|
|
317
325
|
let emailModel;
|
|
@@ -3,12 +3,17 @@ const {ValidationError} = require('@tryghost/errors');
|
|
|
3
3
|
|
|
4
4
|
class SingleUseTokenProvider {
|
|
5
5
|
/**
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {
|
|
6
|
+
* @param {Object} dependencies
|
|
7
|
+
* @param {import('../../models/base')} dependencies.SingleUseTokenModel - A model for creating and retrieving tokens.
|
|
8
|
+
* @param {number} dependencies.validityPeriod - How long a token is valid for from it's creation in milliseconds.
|
|
9
|
+
* @param {number} dependencies.validityPeriodAfterUsage - How long a token is valid after first usage, in milliseconds.
|
|
10
|
+
* @param {number} dependencies.maxUsageCount - How many times a token can be used.
|
|
8
11
|
*/
|
|
9
|
-
constructor(SingleUseTokenModel,
|
|
12
|
+
constructor({SingleUseTokenModel, validityPeriod, validityPeriodAfterUsage, maxUsageCount}) {
|
|
10
13
|
this.model = SingleUseTokenModel;
|
|
11
|
-
this.
|
|
14
|
+
this.validityPeriod = validityPeriod;
|
|
15
|
+
this.validityPeriodAfterUsage = validityPeriodAfterUsage;
|
|
16
|
+
this.maxUsageCount = maxUsageCount;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
/**
|
|
@@ -37,8 +42,17 @@ class SingleUseTokenProvider {
|
|
|
37
42
|
*
|
|
38
43
|
* @returns {Promise<Object<string, any>>}
|
|
39
44
|
*/
|
|
40
|
-
async validate(token) {
|
|
41
|
-
|
|
45
|
+
async validate(token, options = {}) {
|
|
46
|
+
if (!options.transacting) {
|
|
47
|
+
return await this.model.transaction((transacting) => {
|
|
48
|
+
return this.validate(token, {
|
|
49
|
+
...options,
|
|
50
|
+
transacting
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const model = await this.model.findOne({token}, {transacting: options.transacting, forUpdate: true});
|
|
42
56
|
|
|
43
57
|
if (!model) {
|
|
44
58
|
throw new ValidationError({
|
|
@@ -46,16 +60,46 @@ class SingleUseTokenProvider {
|
|
|
46
60
|
});
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
if (model.get('used_count') >= this.maxUsageCount) {
|
|
64
|
+
throw new ValidationError({
|
|
65
|
+
message: 'Token expired'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
const createdAtEpoch = model.get('created_at').getTime();
|
|
70
|
+
const firstUsedAtEpoch = model.get('first_used_at')?.getTime() ?? createdAtEpoch;
|
|
71
|
+
|
|
72
|
+
// Is this token already used?
|
|
73
|
+
if (model.get('used_count') > 0) {
|
|
74
|
+
const timeSinceFirstUsage = Date.now() - firstUsedAtEpoch;
|
|
50
75
|
|
|
76
|
+
if (timeSinceFirstUsage > this.validityPeriodAfterUsage) {
|
|
77
|
+
throw new ValidationError({
|
|
78
|
+
message: 'Token expired'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
51
82
|
const tokenLifetimeMilliseconds = Date.now() - createdAtEpoch;
|
|
52
83
|
|
|
53
|
-
if (tokenLifetimeMilliseconds > this.
|
|
84
|
+
if (tokenLifetimeMilliseconds > this.validityPeriod) {
|
|
54
85
|
throw new ValidationError({
|
|
55
86
|
message: 'Token expired'
|
|
56
87
|
});
|
|
57
88
|
}
|
|
58
89
|
|
|
90
|
+
if (!model.get('first_used_at')) {
|
|
91
|
+
await model.save({
|
|
92
|
+
first_used_at: new Date(),
|
|
93
|
+
updated_at: new Date(),
|
|
94
|
+
used_count: model.get('used_count') + 1
|
|
95
|
+
}, {autoRefresh: false, patch: true, transacting: options.transacting});
|
|
96
|
+
} else {
|
|
97
|
+
await model.save({
|
|
98
|
+
used_count: model.get('used_count') + 1,
|
|
99
|
+
updated_at: new Date()
|
|
100
|
+
}, {autoRefresh: false, patch: true, transacting: options.transacting});
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
try {
|
|
60
104
|
return JSON.parse(model.get('data'));
|
|
61
105
|
} catch (err) {
|
|
@@ -19,6 +19,8 @@ const memberAttributionService = require('../member-attribution');
|
|
|
19
19
|
const emailSuppressionList = require('../email-suppression-list');
|
|
20
20
|
|
|
21
21
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
22
|
+
const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000;
|
|
23
|
+
const MAGIC_LINK_TOKEN_MAX_USAGE_COUNT = 3;
|
|
22
24
|
|
|
23
25
|
const ghostMailer = new mail.GhostMailer();
|
|
24
26
|
|
|
@@ -30,7 +32,12 @@ function createApiInstance(config) {
|
|
|
30
32
|
auth: {
|
|
31
33
|
getSigninURL: config.getSigninURL.bind(config),
|
|
32
34
|
allowSelfSignup: config.getAllowSelfSignup.bind(config),
|
|
33
|
-
tokenProvider: new SingleUseTokenProvider(
|
|
35
|
+
tokenProvider: new SingleUseTokenProvider({
|
|
36
|
+
SingleUseTokenModel: models.SingleUseToken,
|
|
37
|
+
validityPeriod: MAGIC_LINK_TOKEN_VALIDITY,
|
|
38
|
+
validityPeriodAfterUsage: MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE,
|
|
39
|
+
maxUsageCount: MAGIC_LINK_TOKEN_MAX_USAGE_COUNT
|
|
40
|
+
})
|
|
34
41
|
},
|
|
35
42
|
mail: {
|
|
36
43
|
transporter: {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const {parentPort} = require('worker_threads');
|
|
2
|
+
const debug = require('@tryghost/debug')('jobs:clean-tokens');
|
|
3
|
+
const moment = require('moment');
|
|
4
|
+
|
|
5
|
+
// Exit early when cancelled to prevent stalling shutdown. No cleanup needed when cancelling as everything is idempotent and will pick up
|
|
6
|
+
// where it left off on next run
|
|
7
|
+
function cancel() {
|
|
8
|
+
if (parentPort) {
|
|
9
|
+
parentPort.postMessage('Expired complimentary subscriptions cleanup cancelled before completion');
|
|
10
|
+
parentPort.postMessage('cancelled');
|
|
11
|
+
} else {
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}, 1000);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (parentPort) {
|
|
19
|
+
parentPort.once('message', (message) => {
|
|
20
|
+
if (message === 'cancel') {
|
|
21
|
+
return cancel();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
(async () => {
|
|
27
|
+
const cleanupStartDate = new Date();
|
|
28
|
+
const db = require('../../../data/db');
|
|
29
|
+
debug(`Starting cleanup of tokens`);
|
|
30
|
+
|
|
31
|
+
// We delete all tokens that are older than 24 hours.
|
|
32
|
+
const d = moment.utc().subtract(24, 'hours');
|
|
33
|
+
const deletedTokens = await db.knex('tokens')
|
|
34
|
+
.where('created_at', '<', d.format('YYYY-MM-DD HH:mm:ss')) // we need to be careful about the type here. .format() is the only thing that works across SQLite and MySQL
|
|
35
|
+
.delete();
|
|
36
|
+
|
|
37
|
+
const cleanupEndDate = new Date();
|
|
38
|
+
|
|
39
|
+
debug(`Removed ${deletedTokens} tokens created before ${d.toISOString()} in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
|
|
40
|
+
|
|
41
|
+
if (parentPort) {
|
|
42
|
+
parentPort.postMessage(`Removed ${deletedTokens} tokens created before ${d.toISOString()} in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
|
|
43
|
+
parentPort.postMessage('done');
|
|
44
|
+
} else {
|
|
45
|
+
// give the logging pipes time finish writing before exit
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}, 1000);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const jobsService = require('../../jobs');
|
|
3
3
|
|
|
4
|
-
let hasScheduled =
|
|
4
|
+
let hasScheduled = {
|
|
5
|
+
expiredComped: false,
|
|
6
|
+
tokens: false
|
|
7
|
+
};
|
|
5
8
|
|
|
6
9
|
module.exports = {
|
|
7
10
|
async scheduleExpiredCompCleanupJob() {
|
|
8
11
|
if (
|
|
9
|
-
!hasScheduled &&
|
|
12
|
+
!hasScheduled.expiredComped &&
|
|
10
13
|
!process.env.NODE_ENV.startsWith('test')
|
|
11
14
|
) {
|
|
12
15
|
// use a random seconds value to avoid spikes to external APIs on the minute
|
|
@@ -19,9 +22,31 @@ module.exports = {
|
|
|
19
22
|
name: 'clean-expired-comped'
|
|
20
23
|
});
|
|
21
24
|
|
|
22
|
-
hasScheduled = true;
|
|
25
|
+
hasScheduled.expiredComped = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return hasScheduled.expiredComped;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async scheduleTokenCleanupJob() {
|
|
32
|
+
if (
|
|
33
|
+
!hasScheduled.tokens &&
|
|
34
|
+
!process.env.NODE_ENV.startsWith('test')
|
|
35
|
+
) {
|
|
36
|
+
// use a random seconds/minutes/hours value to avoid delete spikes to the database
|
|
37
|
+
const s = Math.floor(Math.random() * 60); // 0-59
|
|
38
|
+
const m = Math.floor(Math.random() * 60); // 0-59
|
|
39
|
+
const h = Math.floor(Math.random() * 24); // 0-23
|
|
40
|
+
|
|
41
|
+
jobsService.addJob({
|
|
42
|
+
at: `${s} ${m} ${h} * * *`, // Every day
|
|
43
|
+
job: require('path').resolve(__dirname, 'clean-tokens.js'),
|
|
44
|
+
name: 'clean-tokens'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
hasScheduled.tokens = true;
|
|
23
48
|
}
|
|
24
49
|
|
|
25
|
-
return hasScheduled;
|
|
50
|
+
return hasScheduled.tokens;
|
|
26
51
|
}
|
|
27
52
|
};
|