ghost 4.37.0 → 4.38.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.
Files changed (81) hide show
  1. package/content/themes/casper/LICENSE +1 -1
  2. package/content/themes/casper/README.md +1 -1
  3. package/content/themes/casper/assets/built/global.css +1 -1
  4. package/content/themes/casper/assets/built/global.css.map +1 -1
  5. package/content/themes/casper/assets/built/screen.css +1 -1
  6. package/content/themes/casper/assets/built/screen.css.map +1 -1
  7. package/content/themes/casper/assets/css/global.css +14 -6
  8. package/content/themes/casper/assets/css/screen.css +9 -1
  9. package/content/themes/casper/package.json +2 -2
  10. package/content/themes/casper/partials/post-card.hbs +1 -1
  11. package/content/themes/casper/post.hbs +18 -19
  12. package/content/themes/casper/yarn.lock +186 -217
  13. package/core/built/assets/ghost-dark-9f760f16230b8bc52e188d6ce28516b0.css +1 -0
  14. package/core/built/assets/{ghost.min-c1938f6ee696bf08bd6bf93cac341ea2.js → ghost.min-6386b02480494a69c3bfe66206754836.js} +339 -279
  15. package/core/built/assets/ghost.min-f4c59dd57a2136df8b0a34f87c099034.css +1 -0
  16. package/core/built/assets/icons/eye.svg +4 -1
  17. package/core/built/assets/icons/member-add.svg +3 -0
  18. package/core/built/assets/icons/pin.svg +4 -1
  19. package/core/built/assets/{vendor.min-6dc30be68238b5c55df0cdc1f2dc8b8d.js → vendor.min-c814d3c4b3f543c4cd5ef3aacd0fc645.js} +36 -34
  20. package/core/frontend/helpers/get.js +4 -0
  21. package/core/frontend/helpers/match.js +12 -0
  22. package/core/frontend/helpers/prev_post.js +11 -1
  23. package/core/frontend/helpers/tiers.js +59 -0
  24. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  25. package/core/frontend/services/routing/router-manager.js +1 -1
  26. package/core/frontend/web/site.js +10 -0
  27. package/core/server/api/canary/index.js +4 -0
  28. package/core/server/api/canary/members.js +2 -1
  29. package/core/server/api/canary/products.js +3 -6
  30. package/core/server/api/canary/tiers-public.js +34 -0
  31. package/core/server/api/canary/tiers.js +3 -6
  32. package/core/server/api/canary/utils/serializers/output/email-posts.js +7 -1
  33. package/core/server/api/canary/utils/serializers/output/pages.js +9 -2
  34. package/core/server/api/canary/utils/serializers/output/posts.js +8 -2
  35. package/core/server/api/canary/utils/serializers/output/preview.js +7 -1
  36. package/core/server/api/canary/utils/serializers/output/products.js +3 -1
  37. package/core/server/api/canary/utils/serializers/output/tiers.js +4 -2
  38. package/core/server/api/canary/utils/serializers/output/utils/mapper.js +17 -7
  39. package/core/server/data/db/connection.js +3 -2
  40. package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -1
  41. package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -1
  42. package/core/server/data/migrations/versions/3.29/03-remove-orphaned-customers.js +2 -1
  43. package/core/server/data/migrations/versions/3.29/04-remove-orphaned-subscriptions.js +2 -1
  44. package/core/server/data/migrations/versions/3.29/05-add-member-constraints.js +3 -2
  45. package/core/server/data/migrations/versions/3.39/06-add-email-recipient-index.js +4 -3
  46. package/core/server/data/migrations/versions/4.0/14-remove-orphaned-stripe-records.js +2 -1
  47. package/core/server/data/migrations/versions/4.0/26-add-cascade-on-delete.js +2 -1
  48. package/core/server/data/migrations/versions/4.0/29-fix-foreign-key-for-members-stripe-customers-subscriptions.js +2 -1
  49. package/core/server/data/migrations/versions/4.1/02-add-unique-constraint-for-member-stripe-tables.js +2 -1
  50. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +3 -2
  51. package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +2 -2
  52. package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +2 -1
  53. package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +2 -1
  54. package/core/server/data/migrations/versions/4.37/2022-02-21-09-53-backfill-members-last-seen-at-column.js +3 -2
  55. package/core/server/data/migrations/versions/4.38/2022-03-01-08-46-add-visibility-to-tiers.js +11 -0
  56. package/core/server/data/migrations/versions/4.38/2022-03-03-16-12-add-visibility-to-tiers.js +8 -0
  57. package/core/server/data/migrations/versions/4.38/2022-03-03-16-17-drop-tiers-visible-column.js +7 -0
  58. package/core/server/data/schema/clients/index.js +1 -1
  59. package/core/server/data/schema/clients/mysql.js +4 -4
  60. package/core/server/data/schema/commands.js +42 -50
  61. package/core/server/data/schema/fixtures/fixtures.json +4 -2
  62. package/core/server/data/schema/schema.js +7 -0
  63. package/core/server/models/product.js +2 -1
  64. package/core/server/services/auth/api-key/admin.js +15 -6
  65. package/core/server/services/auth/setup.js +5 -0
  66. package/core/server/services/email-analytics/lib/event-processor.js +18 -1
  67. package/core/server/services/members/middleware.js +3 -0
  68. package/core/server/services/members/service.js +13 -1
  69. package/core/server/web/admin/views/default-prod.html +4 -4
  70. package/core/server/web/admin/views/default.html +4 -4
  71. package/core/server/web/api/app.js +3 -0
  72. package/core/server/web/api/canary/admin/middleware.js +2 -0
  73. package/core/server/web/api/canary/content/routes.js +1 -0
  74. package/core/server/web/members/app.js +1 -1
  75. package/core/shared/config/defaults.json +2 -2
  76. package/core/shared/config/utils.js +5 -1
  77. package/core/shared/labs.js +1 -0
  78. package/package.json +47 -44
  79. package/yarn.lock +606 -574
  80. package/core/built/assets/ghost-dark-d54723f7267e66fa2595f897076e86c2.css +0 -1
  81. package/core/built/assets/ghost.min-02a5f8954bd85fe28817b8c8b111b8aa.css +0 -1
@@ -4,6 +4,7 @@ const logging = require('@tryghost/logging');
4
4
  const errors = require('@tryghost/errors');
5
5
  const tpl = require('@tryghost/tpl');
6
6
  const db = require('../db');
7
+ const DatabaseInfo = require('@tryghost/database-info');
7
8
  const schema = require('./schema');
8
9
  const clients = require('./clients');
9
10
 
@@ -58,14 +59,14 @@ function addTableColumn(tableName, table, columnName, columnSpec = schema[tableN
58
59
  }
59
60
  }
60
61
 
61
- function addColumn(tableName, column, transaction, columnSpec) {
62
- return (transaction || db.knex).schema.table(tableName, function (table) {
62
+ function addColumn(tableName, column, transaction = db.knex, columnSpec) {
63
+ return transaction.schema.table(tableName, function (table) {
63
64
  addTableColumn(tableName, table, column, columnSpec);
64
65
  });
65
66
  }
66
67
 
67
- function dropColumn(tableName, column, transaction) {
68
- return (transaction || db.knex).schema.table(tableName, function (table) {
68
+ function dropColumn(tableName, column, transaction = db.knex) {
69
+ return transaction.schema.table(tableName, function (table) {
69
70
  table.dropColumn(column);
70
71
  });
71
72
  }
@@ -77,11 +78,11 @@ function dropColumn(tableName, column, transaction) {
77
78
  * @param {string|[string]} columns - column(s) to form unique constraint with
78
79
  * @param {import('knex')} transaction - connection object containing knex reference
79
80
  */
80
- async function addUnique(tableName, columns, transaction) {
81
+ async function addUnique(tableName, columns, transaction = db.knex) {
81
82
  try {
82
83
  logging.info(`Adding unique constraint for: ${columns} in table ${tableName}`);
83
84
 
84
- return await (transaction || db.knex).schema.table(tableName, function (table) {
85
+ return await transaction.schema.table(tableName, function (table) {
85
86
  table.unique(columns);
86
87
  });
87
88
  } catch (err) {
@@ -104,11 +105,11 @@ async function addUnique(tableName, columns, transaction) {
104
105
  * @param {string|[string]} columns - column(s) unique constraint was formed
105
106
  * @param {import('knex')} transaction - connection object containing knex reference
106
107
  */
107
- async function dropUnique(tableName, columns, transaction) {
108
+ async function dropUnique(tableName, columns, transaction = db.knex) {
108
109
  try {
109
110
  logging.info(`Dropping unique constraint for: ${columns} in table: ${tableName}`);
110
111
 
111
- return await (transaction || db.knex).schema.table(tableName, function (table) {
112
+ return await transaction.schema.table(tableName, function (table) {
112
113
  table.dropUnique(columns);
113
114
  });
114
115
  } catch (err) {
@@ -134,17 +135,14 @@ async function dropUnique(tableName, columns, transaction) {
134
135
  * @param {string} configuration.toColumn - column of the table to point the foreign key to
135
136
  * @param {import('knex')} configuration.transaction - connection object containing knex reference
136
137
  */
137
- async function hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction}) {
138
- const knex = (transaction || db.knex);
139
- const client = knex.client.config.client;
140
-
141
- if (client !== 'sqlite3') {
138
+ async function hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction = db.knex}) {
139
+ if (!DatabaseInfo.isSQLite(transaction)) {
142
140
  throw new errors.InternalServerError({
143
141
  message: tpl(messages.hasForeignSQLite3)
144
142
  });
145
143
  }
146
144
 
147
- const foreignKeys = await knex.raw(`PRAGMA foreign_key_list('${fromTable}');`);
145
+ const foreignKeys = await transaction.raw(`PRAGMA foreign_key_list('${fromTable}');`);
148
146
 
149
147
  const hasForeignKey = foreignKeys.some(foreignKey => foreignKey.table === toTable && foreignKey.from === fromColumn && foreignKey.to === toColumn);
150
148
 
@@ -162,9 +160,8 @@ async function hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, trans
162
160
  * @param {Boolean} configuration.cascadeDelete - adds the "on delete cascade" option if true
163
161
  * @param {import('knex')} configuration.transaction - connection object containing knex reference
164
162
  */
165
- async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDelete = false, transaction}) {
166
- const isSQLite = db.knex.client.config.client === 'sqlite3';
167
- if (isSQLite) {
163
+ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDelete = false, transaction = db.knex}) {
164
+ if (DatabaseInfo.isSQLite(transaction)) {
168
165
  const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
169
166
  if (foreignKeyExists) {
170
167
  logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key already exists`);
@@ -176,14 +173,14 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
176
173
 
177
174
  //disable and re-enable foreign key checks on sqlite because of https://github.com/knex/knex/issues/4155
178
175
  let foreignKeysEnabled;
179
- if (isSQLite) {
176
+ if (DatabaseInfo.isSQLite(transaction)) {
180
177
  foreignKeysEnabled = await db.knex.raw('PRAGMA foreign_keys;');
181
178
  if (foreignKeysEnabled[0].foreign_keys) {
182
179
  await db.knex.raw('PRAGMA foreign_keys = OFF;');
183
180
  }
184
181
  }
185
182
 
186
- await (transaction || db.knex).schema.table(fromTable, function (table) {
183
+ await transaction.schema.table(fromTable, function (table) {
187
184
  if (cascadeDelete) {
188
185
  table.foreign(fromColumn).references(`${toTable}.${toColumn}`).onDelete('CASCADE');
189
186
  } else {
@@ -191,13 +188,13 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
191
188
  }
192
189
  });
193
190
 
194
- if (isSQLite) {
191
+ if (DatabaseInfo.isSQLite(transaction)) {
195
192
  if (foreignKeysEnabled[0].foreign_keys) {
196
193
  await db.knex.raw('PRAGMA foreign_keys = ON;');
197
194
  }
198
195
  }
199
196
  } catch (err) {
200
- if (err.code === 'ER_DUP_KEY') {
197
+ if (err.code === 'ER_DUP_KEY' || err.code === 'ER_FK_DUP_KEY' || err.code === 'ER_FK_DUP_NAME') {
201
198
  logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key already exists`);
202
199
  return;
203
200
  }
@@ -215,9 +212,8 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
215
212
  * @param {string} configuration.toColumn - column of the table to point the foreign key to
216
213
  * @param {import('knex')} configuration.transaction - connection object containing knex reference
217
214
  */
218
- async function dropForeign({fromTable, fromColumn, toTable, toColumn, transaction}) {
219
- const isSQLite = db.knex.client.config.client === 'sqlite3';
220
- if (isSQLite) {
215
+ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transaction = db.knex}) {
216
+ if (DatabaseInfo.isSQLite(transaction)) {
221
217
  const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
222
218
  if (!foreignKeyExists) {
223
219
  logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key does not exist`);
@@ -229,18 +225,18 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
229
225
 
230
226
  //disable and re-enable foreign key checks on sqlite because of https://github.com/knex/knex/issues/4155
231
227
  let foreignKeysEnabled;
232
- if (isSQLite) {
228
+ if (DatabaseInfo.isSQLite(transaction)) {
233
229
  foreignKeysEnabled = await db.knex.raw('PRAGMA foreign_keys;');
234
230
  if (foreignKeysEnabled[0].foreign_keys) {
235
231
  await db.knex.raw('PRAGMA foreign_keys = OFF;');
236
232
  }
237
233
  }
238
234
 
239
- await (transaction || db.knex).schema.table(fromTable, function (table) {
235
+ await transaction.schema.table(fromTable, function (table) {
240
236
  table.dropForeign(fromColumn);
241
237
  });
242
238
 
243
- if (isSQLite) {
239
+ if (DatabaseInfo.isSQLite(transaction)) {
244
240
  if (foreignKeysEnabled[0].foreign_keys) {
245
241
  await db.knex.raw('PRAGMA foreign_keys = ON;');
246
242
  }
@@ -260,17 +256,14 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
260
256
  * @param {string} tableName - name of the table to check primary key constraint on
261
257
  * @param {import('knex')} transaction - connection object containing knex reference
262
258
  */
263
- async function hasPrimaryKeySQLite(tableName, transaction) {
264
- const knex = (transaction || db.knex);
265
- const client = knex.client.config.client;
266
-
267
- if (client !== 'sqlite3') {
259
+ async function hasPrimaryKeySQLite(tableName, transaction = db.knex) {
260
+ if (!DatabaseInfo.isSQLite(transaction)){
268
261
  throw new errors.InternalServerError({
269
262
  message: tpl(messages.hasPrimaryKeySQLiteError)
270
263
  });
271
264
  }
272
265
 
273
- const rawConstraints = await knex.raw(`PRAGMA index_list('${tableName}');`);
266
+ const rawConstraints = await transaction.raw(`PRAGMA index_list('${tableName}');`);
274
267
  const tablePrimaryKey = rawConstraints.find(c => c.origin === 'pk');
275
268
 
276
269
  return tablePrimaryKey;
@@ -283,9 +276,8 @@ async function hasPrimaryKeySQLite(tableName, transaction) {
283
276
  * @param {string|[string]} columns - column(s) to form primary key constraint with
284
277
  * @param {import('knex')} transaction - connection object containing knex reference
285
278
  */
286
- async function addPrimaryKey(tableName, columns, transaction) {
287
- const isSQLite = db.knex.client.config.client === 'sqlite3';
288
- if (isSQLite) {
279
+ async function addPrimaryKey(tableName, columns, transaction = db.knex) {
280
+ if (DatabaseInfo.isSQLite(transaction)) {
289
281
  const primaryKeyExists = await hasPrimaryKeySQLite(tableName, transaction);
290
282
  if (primaryKeyExists) {
291
283
  logging.warn(`Primary key constraint for: ${columns} already exists for table: ${tableName}`);
@@ -294,7 +286,7 @@ async function addPrimaryKey(tableName, columns, transaction) {
294
286
  }
295
287
  try {
296
288
  logging.info(`Adding primary key constraint for: ${columns} in table ${tableName}`);
297
- return await (transaction || db.knex).schema.table(tableName, function (table) {
289
+ return await transaction.schema.table(tableName, function (table) {
298
290
  table.primary(columns);
299
291
  });
300
292
  } catch (err) {
@@ -316,8 +308,8 @@ async function addPrimaryKey(tableName, columns, transaction) {
316
308
  * @param {import('knex').Transaction} transaction - connection to the DB
317
309
  * @param {Object} [tableSpec] - table schema to generate table with
318
310
  */
319
- function createTable(table, transaction, tableSpec = schema[table]) {
320
- return (transaction || db.knex).schema.createTable(table, function (t) {
311
+ function createTable(table, transaction = db.knex, tableSpec = schema[table]) {
312
+ return transaction.schema.createTable(table, function (t) {
321
313
  Object.keys(tableSpec)
322
314
  .filter(column => !(column.startsWith('@@')))
323
315
  .forEach(column => addTableColumn(table, t, column, tableSpec[column]));
@@ -331,12 +323,12 @@ function createTable(table, transaction, tableSpec = schema[table]) {
331
323
  });
332
324
  }
333
325
 
334
- function deleteTable(table, transaction) {
335
- return (transaction || db.knex).schema.dropTableIfExists(table);
326
+ function deleteTable(table, transaction = db.knex) {
327
+ return transaction.schema.dropTableIfExists(table);
336
328
  }
337
329
 
338
- function getTables(transaction) {
339
- const client = (transaction || db.knex).client.config.client;
330
+ function getTables(transaction = db.knex) {
331
+ const client = transaction.client.config.client;
340
332
 
341
333
  if (_.includes(_.keys(clients), client)) {
342
334
  return clients[client].getTables(transaction);
@@ -345,8 +337,8 @@ function getTables(transaction) {
345
337
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
346
338
  }
347
339
 
348
- function getIndexes(table, transaction) {
349
- const client = (transaction || db.knex).client.config.client;
340
+ function getIndexes(table, transaction = db.knex) {
341
+ const client = transaction.client.config.client;
350
342
 
351
343
  if (_.includes(_.keys(clients), client)) {
352
344
  return clients[client].getIndexes(table, transaction);
@@ -355,8 +347,8 @@ function getIndexes(table, transaction) {
355
347
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
356
348
  }
357
349
 
358
- function getColumns(table, transaction) {
359
- const client = (transaction || db.knex).client.config.client;
350
+ function getColumns(table, transaction = db.knex) {
351
+ const client = transaction.client.config.client;
360
352
 
361
353
  if (_.includes(_.keys(clients), client)) {
362
354
  return clients[client].getColumns(table);
@@ -365,10 +357,10 @@ function getColumns(table, transaction) {
365
357
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
366
358
  }
367
359
 
368
- function checkTables(transaction) {
369
- const client = (transaction || db.knex).client.config.client;
360
+ function checkTables(transaction = db.knex) {
361
+ const client = transaction.client.config.client;
370
362
 
371
- if (client === 'mysql') {
363
+ if (DatabaseInfo.isMySQL(transaction)) {
372
364
  return clients[client].checkPostTable();
373
365
  }
374
366
  }
@@ -7,13 +7,15 @@
7
7
  "name": "Free",
8
8
  "slug": "free",
9
9
  "type": "free",
10
- "active": true
10
+ "active": true,
11
+ "visibility": "public"
11
12
  },
12
13
  {
13
14
  "name": "Default Product",
14
15
  "slug": "default-product",
15
16
  "type": "paid",
16
- "active": true
17
+ "active": true,
18
+ "visibility": "public"
17
19
  }
18
20
  ]
19
21
  },
@@ -381,6 +381,13 @@ module.exports = {
381
381
  slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
382
382
  active: {type: 'boolean', nullable: false, defaultTo: true},
383
383
  welcome_page_url: {type: 'string', maxlength: 2000, nullable: true},
384
+ visibility: {
385
+ type: 'string',
386
+ maxlength: 50,
387
+ nullable: false,
388
+ defaultTo: 'none',
389
+ validations: {isIn: [['public', 'none']]}
390
+ },
384
391
  monthly_price_id: {type: 'string', maxlength: 24, nullable: true},
385
392
  yearly_price_id: {type: 'string', maxlength: 24, nullable: true},
386
393
  description: {type: 'string', maxlength: 191, nullable: true},
@@ -5,7 +5,8 @@ const Product = ghostBookshelf.Model.extend({
5
5
  tableName: 'products',
6
6
 
7
7
  defaults: {
8
- active: true
8
+ active: true,
9
+ visibility: 'none'
9
10
  },
10
11
 
11
12
  relationships: ['benefits'],
@@ -3,6 +3,7 @@ const url = require('url');
3
3
  const models = require('../../../models');
4
4
  const errors = require('@tryghost/errors');
5
5
  const limitService = require('../../../services/limits');
6
+ const config = require('../../../../shared/config');
6
7
  const tpl = require('@tryghost/tpl');
7
8
  const _ = require('lodash');
8
9
 
@@ -139,12 +140,20 @@ const authenticateWithToken = async (req, res, next, {token, JWT_OPTIONS}) => {
139
140
  const secret = Buffer.from(apiKey.get('secret'), 'hex');
140
141
 
141
142
  const {pathname} = url.parse(req.originalUrl);
142
- const [hasMatch, version = 'v4', api = 'admin'] = pathname.match(/ghost\/api\/([^/]+)\/([^/]+)\/(.+)*/); // eslint-disable-line no-unused-vars
143
-
144
- // ensure the token was meant for this api version
145
- const options = Object.assign({
146
- audience: new RegExp(`\/?${version}\/${api}\/?$`) // eslint-disable-line no-useless-escape
147
- }, JWT_OPTIONS);
143
+ const [hasMatch, version, api] = pathname.match(/ghost\/api\/([^/]+)\/([^/]+)\/(.+)*/); // eslint-disable-line no-unused-vars
144
+
145
+ // ensure the token was meant for this api
146
+ let options;
147
+ if (!config.get('api:versions:all').includes(version)) {
148
+ // CASE: non-versioned api request
149
+ options = Object.assign({
150
+ audience: new RegExp(`\/?${version}\/?$`) // eslint-disable-line no-useless-escape
151
+ }, JWT_OPTIONS);
152
+ } else {
153
+ options = Object.assign({
154
+ audience: new RegExp(`\/?${version}\/${api}\/?$`) // eslint-disable-line no-useless-escape
155
+ }, JWT_OPTIONS);
156
+ }
148
157
 
149
158
  try {
150
159
  jwt.verify(token, secret, options);
@@ -164,6 +164,11 @@ async function installTheme(data, api) {
164
164
  return data;
165
165
  }
166
166
 
167
+ if (themeName.toLowerCase() === 'tryghost/casper') {
168
+ logging.warn('Skipping theme install as Casper is the default theme.');
169
+ return data;
170
+ }
171
+
167
172
  // Use the api instead of the services as the api performs extra logic
168
173
  try {
169
174
  const installResults = await api.themes.install({
@@ -1,5 +1,5 @@
1
1
  const {EventProcessor} = require('@tryghost/email-analytics-service');
2
- const moment = require('moment');
2
+ const moment = require('moment-timezone');
3
3
 
4
4
  class GhostEventProcessor extends EventProcessor {
5
5
  constructor({db}) {
@@ -88,6 +88,23 @@ class GhostEventProcessor extends EventProcessor {
88
88
  opened_at: this.db.knex.raw('COALESCE(opened_at, ?)', [moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')])
89
89
  });
90
90
 
91
+ // Using the default timezone set in https://github.com/TryGhost/Ghost/blob/2c5643623db0fc4db390f6997c81a73dca7ccacd/core/server/data/schema/default-settings/default-settings.json#L105
92
+ let timezone = 'Etc/UTC';
93
+ const timezoneData = await this.db.knex('settings').first('value').where('key', 'timezone');
94
+ if (timezoneData && timezoneData.value) {
95
+ timezone = timezoneData.value;
96
+ }
97
+
98
+ await this.db.knex('members')
99
+ .where('email', '=', event.recipientEmail)
100
+ .andWhere(builder => builder
101
+ .where('last_seen_at', '<', moment.utc(event.timestamp).tz(timezone).startOf('day').utc().format('YYYY-MM-DD HH:mm:ss'))
102
+ .orWhereNull('last_seen_at')
103
+ )
104
+ .update({
105
+ last_seen_at: moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')
106
+ });
107
+
91
108
  return updateResult > 0;
92
109
  }
93
110
 
@@ -110,9 +110,12 @@ const getPortalProductPrices = async function () {
110
110
  monthlyPrice: product.monthlyPrice,
111
111
  yearlyPrice: product.yearlyPrice,
112
112
  benefits: product.benefits,
113
+ active: product.active,
113
114
  type: product.type,
114
115
  prices: productPrices
115
116
  };
117
+ }).filter((product) => {
118
+ return !!product.active;
116
119
  });
117
120
  const defaultProduct = products.find((product) => {
118
121
  return product.type === 'paid';
@@ -16,6 +16,8 @@ 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 DomainEvents = require('@tryghost/domain-events');
20
+ const {LastSeenAtUpdater} = require('@tryghost/members-events-service');
19
21
  const events = require('../../lib/common/events');
20
22
 
21
23
  const messages = {
@@ -139,7 +141,7 @@ module.exports = {
139
141
  sendVerificationEmail: ({subject, message, amountImported}) => {
140
142
  const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
141
143
  const fromAddress = config.get('user_email');
142
-
144
+
143
145
  if (escalationAddress) {
144
146
  ghostMailer.send({
145
147
  subject,
@@ -158,6 +160,16 @@ module.exports = {
158
160
  eventRepository: membersApi.events
159
161
  });
160
162
 
163
+ new LastSeenAtUpdater({
164
+ models: {
165
+ Member: models.Member
166
+ },
167
+ services: {
168
+ domainEvents: DomainEvents,
169
+ settingsCache
170
+ }
171
+ });
172
+
161
173
  (async () => {
162
174
  try {
163
175
  const collection = await models.SingleUseToken.fetchAll();
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.37%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.38%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -38,7 +38,7 @@
38
38
 
39
39
 
40
40
  <link rel="stylesheet" href="assets/vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css">
41
- <link rel="stylesheet" href="assets/ghost.min-02a5f8954bd85fe28817b8c8b111b8aa.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-f4c59dd57a2136df8b0a34f87c099034.css" title="light">
42
42
 
43
43
 
44
44
 
@@ -56,8 +56,8 @@
56
56
  <div id="ember-basic-dropdown-wormhole"></div>
57
57
 
58
58
 
59
- <script src="assets/vendor.min-6dc30be68238b5c55df0cdc1f2dc8b8d.js"></script>
60
- <script src="assets/ghost.min-c1938f6ee696bf08bd6bf93cac341ea2.js"></script>
59
+ <script src="assets/vendor.min-c814d3c4b3f543c4cd5ef3aacd0fc645.js"></script>
60
+ <script src="assets/ghost.min-6386b02480494a69c3bfe66206754836.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.37%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.38%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -38,7 +38,7 @@
38
38
 
39
39
 
40
40
  <link rel="stylesheet" href="assets/vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css">
41
- <link rel="stylesheet" href="assets/ghost.min-02a5f8954bd85fe28817b8c8b111b8aa.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-f4c59dd57a2136df8b0a34f87c099034.css" title="light">
42
42
 
43
43
 
44
44
 
@@ -56,8 +56,8 @@
56
56
  <div id="ember-basic-dropdown-wormhole"></div>
57
57
 
58
58
 
59
- <script src="assets/vendor.min-6dc30be68238b5c55df0cdc1f2dc8b8d.js"></script>
60
- <script src="assets/ghost.min-c1938f6ee696bf08bd6bf93cac341ea2.js"></script>
59
+ <script src="assets/vendor.min-c814d3c4b3f543c4cd5ef3aacd0fc645.js"></script>
60
+ <script src="assets/ghost.min-6386b02480494a69c3bfe66206754836.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -25,6 +25,9 @@ module.exports = function setupApiApp() {
25
25
  apiApp.lazyUse(urlUtils.getVersionPath({version: 'canary', type: 'content'}), require('./canary/content/app'));
26
26
  apiApp.lazyUse(urlUtils.getVersionPath({version: 'canary', type: 'admin'}), require('./canary/admin/app'));
27
27
 
28
+ apiApp.lazyUse('/content/', require('./canary/content/app'));
29
+ apiApp.lazyUse('/admin/', require('./canary/admin/app'));
30
+
28
31
  // Error handling for requests to non-existent API versions
29
32
  apiApp.use(errorHandler.resourceNotFound);
30
33
  apiApp.use(errorHandler.handleJSONResponse(sentry));
@@ -31,6 +31,8 @@ const notImplemented = function (req, res, next) {
31
31
  members: ['GET', 'PUT', 'DELETE', 'POST'],
32
32
  config: ['GET'],
33
33
  schedules: ['PUT'],
34
+ files: ['POST'],
35
+ media: ['POST'],
34
36
  db: ['POST']
35
37
  };
36
38
 
@@ -34,6 +34,7 @@ module.exports = function apiRoutes() {
34
34
  router.get('/settings', mw.authenticatePublic, http(api.publicSettings.browse));
35
35
 
36
36
  router.get('/products', mw.authenticatePublic, http(api.productsPublic.browse));
37
+ router.get('/tiers', mw.authenticatePublic, http(api.tiersPublic.browse));
37
38
 
38
39
  return router;
39
40
  };
@@ -39,7 +39,7 @@ module.exports = function setupMembersApp() {
39
39
  membersApp.get('/api/session', middleware.getIdentityToken);
40
40
  membersApp.get('/api/offers/:id', middleware.getOfferData);
41
41
  membersApp.delete('/api/session', middleware.deleteSession);
42
- membersApp.get('/api/site', shared.middleware.cacheControl('public', {maxAge: 30}), middleware.getMemberSiteData);
42
+ membersApp.get('/api/site', middleware.getMemberSiteData);
43
43
 
44
44
  // NOTE: this is wrapped in a function to ensure we always go via the getter
45
45
  membersApp.post('/api/send-magic-link', bodyParser.json(), shared.middleware.brute.membersAuth, (req, res, next) => membersService.api.middleware.sendMagicLink(req, res, next));
@@ -128,8 +128,8 @@
128
128
  "emailAnalytics": true
129
129
  },
130
130
  "portal": {
131
- "url": "https://unpkg.com/@tryghost/portal@~1.14.0/umd/portal.min.js",
132
- "version": "1.14"
131
+ "url": "https://unpkg.com/@tryghost/portal@~1.15.0/umd/portal.min.js",
132
+ "version": "1.15"
133
133
  },
134
134
  "tenor": {
135
135
  "publicReadOnlyApiKey": null,
@@ -54,9 +54,13 @@ const checkUrlProtocol = function checkUrlProtocol(url) {
54
54
  * https://github.com/indexzero/nconf/issues/235#issuecomment-257606507
55
55
  */
56
56
  const sanitizeDatabaseProperties = function sanitizeDatabaseProperties(nconf) {
57
+ if (nconf.get('database:client') === 'mysql') {
58
+ nconf.set('database:client', 'mysql2');
59
+ }
60
+
57
61
  const database = nconf.get('database');
58
62
 
59
- if (nconf.get('database:client') === 'mysql') {
63
+ if (nconf.get('database:client') === 'mysql2') {
60
64
  delete database.connection.filename;
61
65
  } else {
62
66
  delete database.connection.host;
@@ -36,6 +36,7 @@ const ALPHA_FEATURES = [
36
36
  'tierName',
37
37
  'membersTableStatus',
38
38
  'membersLastSeenFilter',
39
+ 'membersContainsFilters',
39
40
  'selectablePortalLinks'
40
41
  ];
41
42