ghost 4.34.2 → 4.36.1
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/README.md +1 -1
- package/core/built/assets/ghost-dark-abde961aa8d00ab2696e3ceb0a2ca24b.css +1 -0
- package/core/built/assets/ghost.min-46e075517808b53170b8d9ab0b96d796.css +1 -0
- package/core/built/assets/{ghost.min-4886fb099a526cb6ca5b733bbfbb5d3a.js → ghost.min-f10401ea8fdfee5dcc88cf4dff785a88.js} +1756 -1487
- package/core/built/assets/icons/get-started-members.svg +6 -0
- package/core/built/assets/icons/get-started-migrations.svg +6 -0
- package/core/built/assets/icons/get-started.svg +3 -0
- package/core/built/assets/icons/members-placeholder.svg +5 -1
- package/core/built/assets/icons/pages-placeholder.svg +3 -1
- package/core/built/assets/icons/posts-placeholder.svg +4 -1
- package/core/built/assets/icons/tags-placeholder.svg +5 -1
- package/core/built/assets/img/marketing/members-1-8b89a1f48fe7b336754e91a429531f65.jpg +0 -0
- package/core/built/assets/img/marketing/members-2-791205c82d5cf221f8c99a74f9ee1739.jpg +0 -0
- package/core/built/assets/{vendor.min-079fa61c64e24f0984f2cd7d2ebbf3c3.js → vendor.min-3aa87b4d7b43675386a96b869ed00493.js} +1509 -1441
- package/core/frontend/helpers/cancel_link.js +1 -1
- package/core/frontend/helpers/prev_post.js +1 -1
- package/core/frontend/helpers/products.js +2 -6
- package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
- package/core/frontend/meta/asset-url.js +11 -0
- package/core/frontend/services/routing/controllers/email-post.js +1 -1
- package/core/frontend/services/routing/controllers/preview.js +1 -1
- package/core/frontend/services/routing/controllers/static.js +1 -1
- package/core/frontend/services/routing/helpers/entry-lookup.js +1 -1
- package/core/frontend/services/routing/helpers/fetch-data.js +1 -1
- package/core/frontend/views/unsubscribe.hbs +4 -2
- package/core/server/api/canary/authentication.js +4 -0
- package/core/server/api/canary/email-post.js +1 -1
- package/core/server/api/canary/pages-public.js +1 -1
- package/core/server/api/canary/pages.js +1 -1
- package/core/server/api/canary/posts-public.js +1 -1
- package/core/server/api/canary/posts.js +1 -1
- package/core/server/api/canary/utils/serializers/input/pages.js +1 -9
- package/core/server/api/canary/utils/serializers/input/posts.js +1 -9
- package/core/server/api/canary/utils/serializers/output/email-posts.js +2 -2
- package/core/server/api/canary/utils/serializers/output/pages.js +9 -5
- package/core/server/api/canary/utils/serializers/output/posts.js +9 -5
- package/core/server/api/canary/utils/serializers/output/preview.js +3 -2
- package/core/server/api/canary/utils/serializers/output/products.js +2 -0
- package/core/server/api/canary/utils/serializers/output/utils/clean.js +0 -9
- package/core/server/api/canary/utils/serializers/output/utils/mapper.js +18 -3
- package/core/server/api/canary/utils/validators/input/pages.js +1 -1
- package/core/server/api/canary/utils/validators/input/posts.js +1 -1
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/migrations/versions/4.35/2022-01-20-05-55-add-post-products-table.js +8 -0
- package/core/server/data/migrations/versions/4.35/2022-01-30-15-17-set-welcome-page-url-from-settings.js +45 -0
- package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +18 -0
- package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +18 -0
- package/core/server/data/migrations/versions/4.35/2022-02-02-10-38-add-default-content-visibility-tiers-setting.js +8 -0
- package/core/server/data/migrations/versions/4.35/2022-02-02-13-10-transform-specific-tiers-default-content-visibility.js +147 -0
- package/core/server/data/migrations/versions/4.35/2022-02-04-04-34-populate-empty-portal-products.js +60 -0
- package/core/server/data/migrations/versions/4.36/2022-02-07-14-34-add-last-seen-at-column-to-members.js +10 -0
- package/core/server/data/schema/default-settings.json +4 -0
- package/core/server/data/schema/schema.js +11 -4
- package/core/server/models/post.js +29 -5
- package/core/server/models/settings.js +46 -48
- package/core/server/services/auth/setup.js +37 -1
- package/core/server/services/bulk-email/bulk-email-processor.js +1 -0
- package/core/server/services/mega/template.js +1 -1
- package/core/server/services/members/content-gating.js +9 -1
- package/core/server/services/members/middleware.js +21 -4
- package/core/server/services/members/service.js +30 -4
- package/core/server/services/posts/posts-service.js +25 -1
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/shared/labs.js +3 -2
- package/jsconfig.json +1 -1
- package/package.json +46 -47
- package/yarn.lock +1925 -1842
- package/core/built/assets/ghost-dark-2de4c728f3d2deae25e45092ea0e811f.css +0 -1
- package/core/built/assets/ghost.min-b1d3e45166f2023dd56b35f720636979.css +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Checking default_content_visibility for specific tiers');
|
|
8
|
+
|
|
9
|
+
const settings = await knex('settings')
|
|
10
|
+
.select()
|
|
11
|
+
.whereIn('key', ['default_content_visibility', 'default_content_visibility_tiers']);
|
|
12
|
+
const contentVisibilitySetting = settings.find(d => d.key === 'default_content_visibility');
|
|
13
|
+
const visibilityTiersSetting = settings.find(d => d.key === 'default_content_visibility_tiers');
|
|
14
|
+
if (!contentVisibilitySetting) {
|
|
15
|
+
logging.warn('No default_content_visibility setting found.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!visibilityTiersSetting) {
|
|
20
|
+
logging.warn('No default_content_visibility_tiers setting found.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const contentVisibility = contentVisibilitySetting.value;
|
|
25
|
+
|
|
26
|
+
if (['public', 'members', 'paid'].includes(contentVisibility)) {
|
|
27
|
+
logging.info(`Ignoring default_content_visibility change as already set to ${contentVisibility}.`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Transform visibility to tiers when stored as nql string
|
|
31
|
+
const isValidProductNqlFilter = /^(?:product:[\w-]+,?)+$/.test(contentVisibility);
|
|
32
|
+
const now = knex.raw('CURRENT_TIMESTAMP');
|
|
33
|
+
// Reset visibility value to paid if invalid string/filter
|
|
34
|
+
if (!isValidProductNqlFilter) {
|
|
35
|
+
logging.warn(`Found invalid default_content_visibility value - ${contentVisibility}, resetting to paid`);
|
|
36
|
+
await knex('settings')
|
|
37
|
+
.where({
|
|
38
|
+
key: 'default_content_visibility'
|
|
39
|
+
})
|
|
40
|
+
.update({
|
|
41
|
+
value: 'paid',
|
|
42
|
+
updated_at: now
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
logging.info(`Resetting default_content_visibility_tiers to []`);
|
|
46
|
+
await knex('settings')
|
|
47
|
+
.where({
|
|
48
|
+
key: 'default_content_visibility_tiers'
|
|
49
|
+
})
|
|
50
|
+
.update({
|
|
51
|
+
value: JSON.stringify([]),
|
|
52
|
+
updated_at: now
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// fetch product slugs from nql filter
|
|
58
|
+
const productSlugs = contentVisibility.split(',').map((segment) => {
|
|
59
|
+
return segment.replace('product:', '');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// get product ids for slugs
|
|
63
|
+
const products = await knex('products')
|
|
64
|
+
.select('id')
|
|
65
|
+
.whereIn('slug', productSlugs);
|
|
66
|
+
const productList = products.map((product) => {
|
|
67
|
+
return product.id;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
logging.info(`Updating default_content_visibility to tiers`);
|
|
71
|
+
await knex('settings')
|
|
72
|
+
.where({
|
|
73
|
+
key: 'default_content_visibility'
|
|
74
|
+
})
|
|
75
|
+
.update({
|
|
76
|
+
value: 'tiers',
|
|
77
|
+
updated_at: now
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
logging.info(`Updating default_content_visibility_tiers to ${productList}`);
|
|
81
|
+
await knex('settings')
|
|
82
|
+
.where({
|
|
83
|
+
key: 'default_content_visibility_tiers'
|
|
84
|
+
})
|
|
85
|
+
.update({
|
|
86
|
+
value: JSON.stringify(productList),
|
|
87
|
+
updated_at: now
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
async function down(knex) {
|
|
91
|
+
logging.info('Reverting default_content_visibility for specific tiers');
|
|
92
|
+
|
|
93
|
+
const settings = await knex('settings')
|
|
94
|
+
.select()
|
|
95
|
+
.whereIn('key', ['default_content_visibility', 'default_content_visibility_tiers']);
|
|
96
|
+
const contentVisibilitySetting = settings.find(d => d.key === 'default_content_visibility');
|
|
97
|
+
const visibilityTiersSetting = settings.find(d => d.key === 'default_content_visibility_tiers');
|
|
98
|
+
|
|
99
|
+
const visibilityValue = contentVisibilitySetting && contentVisibilitySetting.value;
|
|
100
|
+
const visibilityTiersValue = visibilityTiersSetting && visibilityTiersSetting.value;
|
|
101
|
+
|
|
102
|
+
if (visibilityValue !== 'tiers') {
|
|
103
|
+
logging.info(`Ignoring default_content_visibility as is set to ${visibilityValue}.`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!visibilityTiersValue) {
|
|
108
|
+
logging.warn(`Ignoring, found empty default_content_visibility_tiers value`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const parsedTiersValue = JSON.parse(visibilityTiersValue);
|
|
114
|
+
const products = await knex('products')
|
|
115
|
+
.select('slug')
|
|
116
|
+
.whereIn('id', parsedTiersValue);
|
|
117
|
+
const productSlugs = products.map((product) => {
|
|
118
|
+
return `product:${product.slug}`;
|
|
119
|
+
}).join(',');
|
|
120
|
+
const now = knex.raw('CURRENT_TIMESTAMP');
|
|
121
|
+
|
|
122
|
+
logging.info(`Setting default_content_visibility to ${productSlugs}`);
|
|
123
|
+
await knex('settings')
|
|
124
|
+
.where({
|
|
125
|
+
key: 'default_content_visibility'
|
|
126
|
+
})
|
|
127
|
+
.update({
|
|
128
|
+
value: productSlugs,
|
|
129
|
+
updated_at: now
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
logging.info(`Setting default_content_visibility_tiers to []`);
|
|
133
|
+
await knex('settings')
|
|
134
|
+
.where({
|
|
135
|
+
key: 'default_content_visibility_tiers'
|
|
136
|
+
})
|
|
137
|
+
.update({
|
|
138
|
+
value: JSON.stringify([]),
|
|
139
|
+
updated_at: now
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
logging.warn(`Invalid default_content_visibility_tiers value - ${visibilityTiersValue}`);
|
|
143
|
+
logging.warn(e);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
package/core/server/data/migrations/versions/4.35/2022-02-04-04-34-populate-empty-portal-products.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
const products = await knex
|
|
7
|
+
.select('id')
|
|
8
|
+
.where({
|
|
9
|
+
type: 'paid'
|
|
10
|
+
})
|
|
11
|
+
.from('products');
|
|
12
|
+
|
|
13
|
+
if (products.length === 0) {
|
|
14
|
+
logging.warn(`Skipping updating portal_products, no product exists`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (products.length > 1) {
|
|
19
|
+
logging.warn(`Skipping updating portal_products, tiers beta is enabled`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const portalProductsSetting = await knex('settings')
|
|
24
|
+
.where('key', 'portal_products')
|
|
25
|
+
.select('value')
|
|
26
|
+
.first();
|
|
27
|
+
|
|
28
|
+
if (!portalProductsSetting) {
|
|
29
|
+
logging.warn(`Missing portal_products setting`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const currPortalProductsValue = JSON.parse(portalProductsSetting.value);
|
|
34
|
+
|
|
35
|
+
if (currPortalProductsValue.length > 0) {
|
|
36
|
+
logging.warn(`Ignoring - portal_products setting is not empty, - ${currPortalProductsValue}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultProduct = products[0];
|
|
41
|
+
const portalProductsValue = [defaultProduct.id];
|
|
42
|
+
|
|
43
|
+
logging.info(`Setting portal_products setting to have product - ${defaultProduct.id}`);
|
|
44
|
+
|
|
45
|
+
const now = knex.raw('CURRENT_TIMESTAMP');
|
|
46
|
+
|
|
47
|
+
await knex('settings')
|
|
48
|
+
.where('key', 'portal_products')
|
|
49
|
+
.update({
|
|
50
|
+
value: JSON.stringify(portalProductsValue),
|
|
51
|
+
updated_at: now
|
|
52
|
+
});
|
|
53
|
+
} catch (e) {
|
|
54
|
+
logging.warn(`Ignoring, unable to parse portal_products setting value - ${portalProductsSetting.value}`);
|
|
55
|
+
logging.warn(e);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
// no-op - we don't want to return to invalid state
|
|
59
|
+
async function down() {}
|
|
60
|
+
);
|
|
@@ -30,8 +30,8 @@ module.exports = {
|
|
|
30
30
|
defaultTo: 'public'
|
|
31
31
|
},
|
|
32
32
|
email_recipient_filter: {
|
|
33
|
-
type: '
|
|
34
|
-
maxlength:
|
|
33
|
+
type: 'text',
|
|
34
|
+
maxlength: 1000000000,
|
|
35
35
|
nullable: false,
|
|
36
36
|
defaultTo: 'none'
|
|
37
37
|
},
|
|
@@ -369,6 +369,7 @@ module.exports = {
|
|
|
369
369
|
email_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
|
|
370
370
|
email_opened_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
|
|
371
371
|
email_open_rate: {type: 'integer', unsigned: true, nullable: true, index: true},
|
|
372
|
+
last_seen_at: {type: 'dateTime',nullable: true},
|
|
372
373
|
created_at: {type: 'dateTime', nullable: false},
|
|
373
374
|
created_by: {type: 'string', maxlength: 24, nullable: false},
|
|
374
375
|
updated_at: {type: 'dateTime', nullable: true},
|
|
@@ -424,6 +425,12 @@ module.exports = {
|
|
|
424
425
|
product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
|
|
425
426
|
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
|
|
426
427
|
},
|
|
428
|
+
posts_products: {
|
|
429
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
430
|
+
post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', cascadeDelete: true},
|
|
431
|
+
product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
|
|
432
|
+
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
|
|
433
|
+
},
|
|
427
434
|
members_payment_events: {
|
|
428
435
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
429
436
|
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
@@ -592,8 +599,8 @@ module.exports = {
|
|
|
592
599
|
validations: {isIn: [['pending', 'submitting', 'submitted', 'failed']]}
|
|
593
600
|
},
|
|
594
601
|
recipient_filter: {
|
|
595
|
-
type: '
|
|
596
|
-
maxlength:
|
|
602
|
+
type: 'text',
|
|
603
|
+
maxlength: 1000000000,
|
|
597
604
|
nullable: false,
|
|
598
605
|
defaultTo: 'status:-free'
|
|
599
606
|
},
|
|
@@ -54,9 +54,20 @@ Post = ghostBookshelf.Model.extend({
|
|
|
54
54
|
*/
|
|
55
55
|
defaults: function defaults() {
|
|
56
56
|
let visibility = 'public';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
let tiers = [];
|
|
58
|
+
const defaultContentVisibility = settingsCache.get('default_content_visibility');
|
|
59
|
+
if (defaultContentVisibility) {
|
|
60
|
+
if (defaultContentVisibility === 'tiers') {
|
|
61
|
+
const tiersData = settingsCache.get('default_content_visibility_tiers') || [];
|
|
62
|
+
visibility = 'tiers',
|
|
63
|
+
tiers = tiersData.map((tierId) => {
|
|
64
|
+
return {
|
|
65
|
+
id: tierId
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
} else if (defaultContentVisibility !== 'tiers') {
|
|
69
|
+
visibility = settingsCache.get('default_content_visibility');
|
|
70
|
+
}
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
return {
|
|
@@ -64,16 +75,18 @@ Post = ghostBookshelf.Model.extend({
|
|
|
64
75
|
status: 'draft',
|
|
65
76
|
featured: false,
|
|
66
77
|
type: 'post',
|
|
78
|
+
tiers,
|
|
67
79
|
visibility: visibility,
|
|
68
80
|
email_recipient_filter: 'none'
|
|
69
81
|
};
|
|
70
82
|
},
|
|
71
83
|
|
|
72
|
-
relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta'],
|
|
84
|
+
relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta', 'tiers'],
|
|
73
85
|
|
|
74
86
|
// NOTE: look up object, not super nice, but was easy to implement
|
|
75
87
|
relationshipBelongsTo: {
|
|
76
88
|
tags: 'tags',
|
|
89
|
+
tiers: 'products',
|
|
77
90
|
authors: 'users',
|
|
78
91
|
posts_meta: 'posts_meta'
|
|
79
92
|
},
|
|
@@ -89,6 +102,17 @@ Post = ghostBookshelf.Model.extend({
|
|
|
89
102
|
}
|
|
90
103
|
},
|
|
91
104
|
|
|
105
|
+
tiers() {
|
|
106
|
+
return this.belongsToMany('Product', 'posts_products', 'post_id', 'product_id')
|
|
107
|
+
.withPivot('sort_order')
|
|
108
|
+
.query('orderBy', 'sort_order', 'ASC')
|
|
109
|
+
.query((qb) => {
|
|
110
|
+
// avoids bookshelf adding a `DISTINCT` to the query
|
|
111
|
+
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
112
|
+
qb.columns('products.*');
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
|
|
92
116
|
parse() {
|
|
93
117
|
const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
|
|
94
118
|
|
|
@@ -171,7 +195,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
171
195
|
|
|
172
196
|
// transform visibility NQL queries to special-case values where necessary
|
|
173
197
|
// ensures checks against special-case values such as `{{#has visibility="paid"}}` continue working
|
|
174
|
-
if (attrs.visibility && !['public', 'members', 'paid'].includes(attrs.visibility)) {
|
|
198
|
+
if (attrs.visibility && !['public', 'members', 'paid', 'tiers'].includes(attrs.visibility)) {
|
|
175
199
|
if (attrs.visibility === 'status:-free') {
|
|
176
200
|
attrs.visibility = 'paid';
|
|
177
201
|
} else {
|
|
@@ -260,62 +260,60 @@ Settings = ghostBookshelf.Model.extend({
|
|
|
260
260
|
await ghostBookshelf.knex.destroy();
|
|
261
261
|
await ghostBookshelf.knex.initialize();
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
const columnInfo = await ghostBookshelf.knex.table('settings').columnInfo();
|
|
265
|
-
const columns = Object.keys(columnInfo);
|
|
266
|
-
|
|
267
|
-
// fetch other data that is used when inserting new settings
|
|
268
|
-
const date = ghostBookshelf.knex.raw('CURRENT_TIMESTAMP');
|
|
269
|
-
let owner;
|
|
270
|
-
try {
|
|
271
|
-
owner = await ghostBookshelf.model('User').getOwnerUser();
|
|
272
|
-
} catch (e) {
|
|
273
|
-
// in some tests the owner is deleted and not recreated before setup
|
|
274
|
-
if (e.errorType === 'NotFoundError') {
|
|
275
|
-
owner = {id: 1};
|
|
276
|
-
} else {
|
|
277
|
-
throw e;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
263
|
+
const allSettings = await this.findAll(options);
|
|
280
264
|
|
|
281
|
-
|
|
282
|
-
.
|
|
283
|
-
|
|
284
|
-
const usedKeys = allSettings.models.map(function mapper(setting) {
|
|
285
|
-
return setting.get('key');
|
|
286
|
-
});
|
|
265
|
+
const usedKeys = allSettings.models.map(function mapper(setting) {
|
|
266
|
+
return setting.get('key');
|
|
267
|
+
});
|
|
287
268
|
|
|
288
|
-
|
|
269
|
+
const settingsToInsert = [];
|
|
289
270
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
271
|
+
_.each(getDefaultSettings(), function forEachDefault(defaultSetting, defaultSettingKey) {
|
|
272
|
+
const isMissingFromDB = usedKeys.indexOf(defaultSettingKey) === -1;
|
|
273
|
+
if (isMissingFromDB) {
|
|
274
|
+
defaultSetting.value = defaultSetting.getDefaultValue();
|
|
275
|
+
settingsToInsert.push(defaultSetting);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
294
278
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
279
|
+
if (settingsToInsert.length > 0) {
|
|
280
|
+
// fetch available columns to avoid populating columns not yet created by migrations
|
|
281
|
+
const columnInfo = await ghostBookshelf.knex.table('settings').columnInfo();
|
|
282
|
+
const columns = Object.keys(columnInfo);
|
|
283
|
+
|
|
284
|
+
// fetch other data that is used when inserting new settings
|
|
285
|
+
const date = ghostBookshelf.knex.raw('CURRENT_TIMESTAMP');
|
|
286
|
+
let owner;
|
|
287
|
+
try {
|
|
288
|
+
owner = await ghostBookshelf.model('User').getOwnerUser();
|
|
289
|
+
} catch (e) {
|
|
290
|
+
// in some tests the owner is deleted and not recreated before setup
|
|
291
|
+
if (e.errorType === 'NotFoundError') {
|
|
292
|
+
owner = {id: 1};
|
|
293
|
+
} else {
|
|
294
|
+
throw e;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
302
297
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
298
|
+
const settingsDataToInsert = settingsToInsert.map((setting) => {
|
|
299
|
+
const settingValues = Object.assign({}, setting, {
|
|
300
|
+
id: ObjectID().toHexString(),
|
|
301
|
+
created_at: date,
|
|
302
|
+
created_by: owner.id,
|
|
303
|
+
updated_at: date,
|
|
304
|
+
updated_by: owner.id
|
|
309
305
|
});
|
|
310
306
|
|
|
311
|
-
|
|
312
|
-
return Promise.all(insertOperations).then(function fetchAllToReturn() {
|
|
313
|
-
return self.findAll(options);
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return allSettings;
|
|
307
|
+
return _.pick(settingValues, columns);
|
|
318
308
|
});
|
|
309
|
+
|
|
310
|
+
await ghostBookshelf.knex
|
|
311
|
+
.batchInsert('settings', settingsDataToInsert);
|
|
312
|
+
|
|
313
|
+
return self.findAll(options);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return allSettings;
|
|
319
317
|
},
|
|
320
318
|
|
|
321
319
|
permissible: function permissible(modelId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
|
|
@@ -149,11 +149,47 @@ function sendWelcomeEmail(email, mailAPI) {
|
|
|
149
149
|
return Promise.resolve();
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
async function installTheme(data, api) {
|
|
153
|
+
const {theme: themeName} = data.userData;
|
|
154
|
+
|
|
155
|
+
if (!themeName) {
|
|
156
|
+
return data;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Use the api instead of the services as the api performs extra logic
|
|
160
|
+
try {
|
|
161
|
+
const installResults = await api.themes.install({
|
|
162
|
+
source: 'github',
|
|
163
|
+
ref: themeName,
|
|
164
|
+
context: {internal: true}
|
|
165
|
+
});
|
|
166
|
+
const theme = installResults.themes[0];
|
|
167
|
+
|
|
168
|
+
await api.themes.activate({
|
|
169
|
+
name: theme.name,
|
|
170
|
+
context: {internal: true}
|
|
171
|
+
});
|
|
172
|
+
} catch (e) {
|
|
173
|
+
//Fallback to Casper by doing nothing as the theme setting update is the last step
|
|
174
|
+
|
|
175
|
+
await api.notifications.add({
|
|
176
|
+
notifications: [{
|
|
177
|
+
custom: true, //avoids update-check from deleting the notification
|
|
178
|
+
type: 'warn',
|
|
179
|
+
message: 'The installation of the theme you have selected wasn\'t successful.'
|
|
180
|
+
}]
|
|
181
|
+
}, {context: {internal: true}});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
|
|
152
187
|
module.exports = {
|
|
153
188
|
checkIsSetup: checkIsSetup,
|
|
154
189
|
assertSetupCompleted: assertSetupCompleted,
|
|
155
190
|
setupUser: setupUser,
|
|
156
191
|
doSettings: doSettings,
|
|
157
192
|
doProduct: doProduct,
|
|
158
|
-
sendWelcomeEmail: sendWelcomeEmail
|
|
193
|
+
sendWelcomeEmail: sendWelcomeEmail,
|
|
194
|
+
installTheme: installTheme
|
|
159
195
|
};
|
|
@@ -1151,7 +1151,7 @@ ${ templateSettings.showBadge ? `
|
|
|
1151
1151
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
1152
1152
|
${ templateSettings.showHeaderIcon && site.iconUrl ? `
|
|
1153
1153
|
<tr>
|
|
1154
|
-
<td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" border="0"></a></td>
|
|
1154
|
+
<td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" alt="${site.title}" border="0"></a></td>
|
|
1155
1155
|
</tr>
|
|
1156
1156
|
` : ``}
|
|
1157
1157
|
${ templateSettings.showHeaderTitle ? `
|
|
@@ -40,7 +40,15 @@ function checkPostAccess(post, member) {
|
|
|
40
40
|
return PERMIT_ACCESS;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
let visibility = post.visibility === 'paid' ? 'status:-free' : post.visibility;
|
|
44
|
+
if (visibility === 'tiers') {
|
|
45
|
+
if (!post.tiers) {
|
|
46
|
+
return BLOCK_ACCESS;
|
|
47
|
+
}
|
|
48
|
+
visibility = post.tiers.map((product) => {
|
|
49
|
+
return `product:${product.slug}`;
|
|
50
|
+
}).join(',');
|
|
51
|
+
}
|
|
44
52
|
|
|
45
53
|
if (visibility && member.status && nql(visibility, {expansions: MEMBER_NQL_EXPANSIONS}).queryJSON(member)) {
|
|
46
54
|
return PERMIT_ACCESS;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const logging = require('@tryghost/logging');
|
|
3
3
|
const membersService = require('./service');
|
|
4
|
+
const models = require('../../models');
|
|
4
5
|
const offersService = require('../offers/service');
|
|
5
6
|
const urlUtils = require('../../../shared/url-utils');
|
|
6
7
|
const ghostVersion = require('@tryghost/version');
|
|
@@ -197,12 +198,28 @@ const createSessionFromMagicLink = async function (req, res, next) {
|
|
|
197
198
|
|
|
198
199
|
const action = req.query.action;
|
|
199
200
|
|
|
200
|
-
if (action === 'signup') {
|
|
201
|
+
if (action === 'signup' || action === 'signup-paid') {
|
|
201
202
|
let customRedirect = '';
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
const mostRecentActiveSubscription = subscriptions
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
const aStartDate = new Date(a.start_date);
|
|
206
|
+
const bStartDate = new Date(b.start_date);
|
|
207
|
+
return bStartDate.valueOf() - aStartDate.valueOf();
|
|
208
|
+
})
|
|
209
|
+
.find(sub => ['active', 'trialing'].includes(sub.status));
|
|
210
|
+
if (mostRecentActiveSubscription) {
|
|
211
|
+
if (labsService.isSet('tierWelcomePages')) {
|
|
212
|
+
customRedirect = mostRecentActiveSubscription.tier.welcome_page_url;
|
|
213
|
+
} else {
|
|
214
|
+
customRedirect = settingsCache.get('members_paid_signup_redirect') || '';
|
|
215
|
+
}
|
|
204
216
|
} else {
|
|
205
|
-
|
|
217
|
+
if (labsService.isSet('tierWelcomePages')) {
|
|
218
|
+
const freeTier = await models.Product.findOne({type: 'free'});
|
|
219
|
+
customRedirect = freeTier && freeTier.get('welcome_page_url') || '';
|
|
220
|
+
} else {
|
|
221
|
+
customRedirect = settingsCache.get('members_free_signup_redirect') || '';
|
|
222
|
+
}
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
if (customRedirect && customRedirect !== '/') {
|
|
@@ -16,13 +16,12 @@ const models = require('../../models');
|
|
|
16
16
|
const {GhostMailer} = require('../mail');
|
|
17
17
|
const jobsService = require('../jobs');
|
|
18
18
|
const VerificationTrigger = require('@tryghost/verification-trigger');
|
|
19
|
+
const events = require('../../lib/common/events');
|
|
19
20
|
|
|
20
21
|
const messages = {
|
|
21
22
|
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
|
|
22
23
|
sslRequiredForStripe: 'Cannot run Ghost without SSL when Stripe is connected. Please update your url config to use "https://".',
|
|
23
|
-
remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
|
|
24
|
-
emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
|
|
25
|
-
emailVerificationEmailMessage: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`
|
|
24
|
+
remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
const ghostMailer = new GhostMailer();
|
|
@@ -63,7 +62,8 @@ const processImport = async (options) => {
|
|
|
63
62
|
if (importSize > importThreshold) {
|
|
64
63
|
await verificationTrigger.startVerificationProcess({
|
|
65
64
|
amountImported: importSize,
|
|
66
|
-
throwOnTrigger: true
|
|
65
|
+
throwOnTrigger: true,
|
|
66
|
+
source: 'import'
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -76,6 +76,32 @@ module.exports = {
|
|
|
76
76
|
const createMembersApiInstance = require('./api');
|
|
77
77
|
const env = config.get('env');
|
|
78
78
|
|
|
79
|
+
events.on('settings.edited', async function (settingModel) {
|
|
80
|
+
if (labsService.isSet('multipleProducts')) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const key = settingModel.get('key');
|
|
85
|
+
const value = settingModel.get('value');
|
|
86
|
+
|
|
87
|
+
if (key === 'members_free_signup_redirect') {
|
|
88
|
+
try {
|
|
89
|
+
await models.Product.forge().query().update('welcome_page_url', value).where('type', 'free');
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logging.error(err);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (key === 'members_paid_signup_redirect') {
|
|
96
|
+
try {
|
|
97
|
+
await models.Product.forge().query().update('welcome_page_url', value).where('type', 'paid');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logging.error(err);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
79
105
|
// @TODO Move to stripe service
|
|
80
106
|
if (env !== 'production') {
|
|
81
107
|
if (stripeService.api.configured && stripeService.api.mode === 'live') {
|