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.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/core/built/assets/ghost-dark-abde961aa8d00ab2696e3ceb0a2ca24b.css +1 -0
  3. package/core/built/assets/ghost.min-46e075517808b53170b8d9ab0b96d796.css +1 -0
  4. package/core/built/assets/{ghost.min-4886fb099a526cb6ca5b733bbfbb5d3a.js → ghost.min-f10401ea8fdfee5dcc88cf4dff785a88.js} +1756 -1487
  5. package/core/built/assets/icons/get-started-members.svg +6 -0
  6. package/core/built/assets/icons/get-started-migrations.svg +6 -0
  7. package/core/built/assets/icons/get-started.svg +3 -0
  8. package/core/built/assets/icons/members-placeholder.svg +5 -1
  9. package/core/built/assets/icons/pages-placeholder.svg +3 -1
  10. package/core/built/assets/icons/posts-placeholder.svg +4 -1
  11. package/core/built/assets/icons/tags-placeholder.svg +5 -1
  12. package/core/built/assets/img/marketing/members-1-8b89a1f48fe7b336754e91a429531f65.jpg +0 -0
  13. package/core/built/assets/img/marketing/members-2-791205c82d5cf221f8c99a74f9ee1739.jpg +0 -0
  14. package/core/built/assets/{vendor.min-079fa61c64e24f0984f2cd7d2ebbf3c3.js → vendor.min-3aa87b4d7b43675386a96b869ed00493.js} +1509 -1441
  15. package/core/frontend/helpers/cancel_link.js +1 -1
  16. package/core/frontend/helpers/prev_post.js +1 -1
  17. package/core/frontend/helpers/products.js +2 -6
  18. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  19. package/core/frontend/meta/asset-url.js +11 -0
  20. package/core/frontend/services/routing/controllers/email-post.js +1 -1
  21. package/core/frontend/services/routing/controllers/preview.js +1 -1
  22. package/core/frontend/services/routing/controllers/static.js +1 -1
  23. package/core/frontend/services/routing/helpers/entry-lookup.js +1 -1
  24. package/core/frontend/services/routing/helpers/fetch-data.js +1 -1
  25. package/core/frontend/views/unsubscribe.hbs +4 -2
  26. package/core/server/api/canary/authentication.js +4 -0
  27. package/core/server/api/canary/email-post.js +1 -1
  28. package/core/server/api/canary/pages-public.js +1 -1
  29. package/core/server/api/canary/pages.js +1 -1
  30. package/core/server/api/canary/posts-public.js +1 -1
  31. package/core/server/api/canary/posts.js +1 -1
  32. package/core/server/api/canary/utils/serializers/input/pages.js +1 -9
  33. package/core/server/api/canary/utils/serializers/input/posts.js +1 -9
  34. package/core/server/api/canary/utils/serializers/output/email-posts.js +2 -2
  35. package/core/server/api/canary/utils/serializers/output/pages.js +9 -5
  36. package/core/server/api/canary/utils/serializers/output/posts.js +9 -5
  37. package/core/server/api/canary/utils/serializers/output/preview.js +3 -2
  38. package/core/server/api/canary/utils/serializers/output/products.js +2 -0
  39. package/core/server/api/canary/utils/serializers/output/utils/clean.js +0 -9
  40. package/core/server/api/canary/utils/serializers/output/utils/mapper.js +18 -3
  41. package/core/server/api/canary/utils/validators/input/pages.js +1 -1
  42. package/core/server/api/canary/utils/validators/input/posts.js +1 -1
  43. package/core/server/data/exporter/table-lists.js +1 -0
  44. package/core/server/data/migrations/versions/4.35/2022-01-20-05-55-add-post-products-table.js +8 -0
  45. package/core/server/data/migrations/versions/4.35/2022-01-30-15-17-set-welcome-page-url-from-settings.js +45 -0
  46. package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +18 -0
  47. package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +18 -0
  48. package/core/server/data/migrations/versions/4.35/2022-02-02-10-38-add-default-content-visibility-tiers-setting.js +8 -0
  49. package/core/server/data/migrations/versions/4.35/2022-02-02-13-10-transform-specific-tiers-default-content-visibility.js +147 -0
  50. package/core/server/data/migrations/versions/4.35/2022-02-04-04-34-populate-empty-portal-products.js +60 -0
  51. package/core/server/data/migrations/versions/4.36/2022-02-07-14-34-add-last-seen-at-column-to-members.js +10 -0
  52. package/core/server/data/schema/default-settings.json +4 -0
  53. package/core/server/data/schema/schema.js +11 -4
  54. package/core/server/models/post.js +29 -5
  55. package/core/server/models/settings.js +46 -48
  56. package/core/server/services/auth/setup.js +37 -1
  57. package/core/server/services/bulk-email/bulk-email-processor.js +1 -0
  58. package/core/server/services/mega/template.js +1 -1
  59. package/core/server/services/members/content-gating.js +9 -1
  60. package/core/server/services/members/middleware.js +21 -4
  61. package/core/server/services/members/service.js +30 -4
  62. package/core/server/services/posts/posts-service.js +25 -1
  63. package/core/server/web/admin/views/default-prod.html +4 -4
  64. package/core/server/web/admin/views/default.html +4 -4
  65. package/core/shared/labs.js +3 -2
  66. package/jsconfig.json +1 -1
  67. package/package.json +46 -47
  68. package/yarn.lock +1925 -1842
  69. package/core/built/assets/ghost-dark-2de4c728f3d2deae25e45092ea0e811f.css +0 -1
  70. 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
+ );
@@ -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
+ );
@@ -0,0 +1,10 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration(
4
+ 'members',
5
+ 'last_seen_at',
6
+ {
7
+ type: 'dateTime',
8
+ nullable: true
9
+ }
10
+ );
@@ -235,6 +235,10 @@
235
235
  "defaultValue": "public",
236
236
  "type": "string"
237
237
  },
238
+ "default_content_visibility_tiers": {
239
+ "defaultValue": "[]",
240
+ "type": "array"
241
+ },
238
242
  "members_signup_access": {
239
243
  "defaultValue": "all",
240
244
  "validations": {
@@ -30,8 +30,8 @@ module.exports = {
30
30
  defaultTo: 'public'
31
31
  },
32
32
  email_recipient_filter: {
33
- type: 'string',
34
- maxlength: 50,
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: 'string',
596
- maxlength: 50,
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
- if (settingsCache.get('default_content_visibility')) {
59
- visibility = settingsCache.get('default_content_visibility');
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
- // fetch available columns to avoid populating columns not yet created by migrations
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
- return this
282
- .findAll(options)
283
- .then(function checkAllSettings(allSettings) {
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
- const insertOperations = [];
269
+ const settingsToInsert = [];
289
270
 
290
- _.each(getDefaultSettings(), function forEachDefault(defaultSetting, defaultSettingKey) {
291
- const isMissingFromDB = usedKeys.indexOf(defaultSettingKey) === -1;
292
- if (isMissingFromDB) {
293
- defaultSetting.value = defaultSetting.getDefaultValue();
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
- const settingValues = Object.assign({}, defaultSetting, {
296
- id: ObjectID().toHexString(),
297
- created_at: date,
298
- created_by: owner.id,
299
- updated_at: date,
300
- updated_by: owner.id
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
- insertOperations.push(
304
- ghostBookshelf.knex
305
- .table('settings')
306
- .insert(_.pick(settingValues, columns))
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
- if (insertOperations.length > 0) {
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
  };
@@ -130,6 +130,7 @@ module.exports = {
130
130
  id: emailModel.id
131
131
  });
132
132
  } catch (err) {
133
+ sentry.captureException(err);
133
134
  logging.error(err);
134
135
  }
135
136
 
@@ -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
- const visibility = post.visibility === 'paid' ? 'status:-free' : post.visibility;
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
- if (subscriptions.find(sub => ['active', 'trialing'].includes(sub.status))) {
203
- customRedirect = settingsCache.get('members_paid_signup_redirect') || '';
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
- customRedirect = settingsCache.get('members_free_signup_redirect') || '';
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') {