ghost 4.41.3 → 4.42.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 (55) hide show
  1. package/core/built/assets/ghost-dark-97613c037232aba4490489431ce170ca.css +1 -0
  2. package/core/built/assets/{ghost.min-1abf114ca26a71e8e1f09054f3592614.js → ghost.min-20096eef632760c3a2906e243adbd24b.js} +400 -323
  3. package/core/built/assets/ghost.min-c08ce1872f0e09edb63eb13c43606d18.css +1 -0
  4. package/core/built/assets/{vendor.min-9094db77ba3190cb10876f8e42e1d90d.js → vendor.min-21f79c68a284acb1b70039f3f63e5507.js} +68 -68
  5. package/core/built/assets/{vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css → vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css} +214 -0
  6. package/core/frontend/helpers/price.js +1 -0
  7. package/core/server/api/canary/email-preview.js +2 -1
  8. package/core/server/api/canary/{email.js → emails.js} +0 -0
  9. package/core/server/api/canary/index.js +9 -1
  10. package/core/server/api/canary/members.js +0 -45
  11. package/core/server/api/canary/newsletters.js +45 -0
  12. package/core/server/api/canary/stats.js +14 -0
  13. package/core/server/api/canary/utils/serializers/output/email-previews.js +7 -0
  14. package/core/server/api/canary/utils/serializers/output/index.js +2 -22
  15. package/core/server/api/canary/utils/serializers/output/mappers/index.js +1 -0
  16. package/core/server/api/canary/utils/serializers/output/mappers/snippets.js +36 -0
  17. package/core/server/api/canary/utils/serializers/output/members.js +0 -2
  18. package/core/server/api/canary/utils/serializers/output/oembed.js +2 -2
  19. package/core/server/api/canary/utils/serializers/output/redirects.js +2 -2
  20. package/core/server/api/canary/utils/serializers/output/schedules.js +2 -2
  21. package/core/server/api/canary/utils/serializers/output/slack.js +2 -2
  22. package/core/server/api/canary/utils/serializers/output/themes.js +2 -2
  23. package/core/server/api/canary/utils/serializers/output/users.js +0 -23
  24. package/core/server/api/shared/serializers/handle.js +25 -26
  25. package/core/server/data/exporter/table-lists.js +1 -0
  26. package/core/server/data/migrations/utils.js +1 -1
  27. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +20 -0
  28. package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js +28 -0
  29. package/core/server/data/schema/commands.js +13 -13
  30. package/core/server/data/schema/schema.js +18 -0
  31. package/core/server/models/newsletter.js +9 -0
  32. package/core/server/services/mega/template.js +25 -13
  33. package/core/server/services/members/service.js +2 -1
  34. package/core/server/services/stats/index.js +1 -0
  35. package/core/server/services/stats/lib/members-stats-service.js +165 -0
  36. package/core/server/services/stats/service.js +6 -0
  37. package/core/server/services/webhooks/webhooks-service.js +3 -1
  38. package/core/server/web/admin/views/default-prod.html +5 -5
  39. package/core/server/web/admin/views/default.html +5 -5
  40. package/core/server/web/api/canary/admin/routes.js +8 -2
  41. package/core/shared/config/defaults.json +2 -2
  42. package/core/shared/config/env/config.development.json +26 -0
  43. package/core/shared/config/env/config.production.json +21 -0
  44. package/core/shared/config/env/config.testing-mysql.json +59 -0
  45. package/core/shared/config/env/config.testing.json +58 -0
  46. package/package.json +38 -38
  47. package/yarn.lock +510 -654
  48. package/core/built/assets/ghost-dark-146c4c688b47d45c4aa018ee0f79cebc.css +0 -1
  49. package/core/built/assets/ghost.min-a73b150c7eecc4641d377cc73fb5eecd.css +0 -1
  50. package/core/server/api/canary/utils/serializers/output/email-preview.js +0 -10
  51. package/core/server/api/canary/utils/serializers/output/emails.js +0 -22
  52. package/core/server/api/canary/utils/serializers/output/identities.js +0 -7
  53. package/core/server/api/canary/utils/serializers/output/member-signin-urls.js +0 -7
  54. package/core/server/api/canary/utils/serializers/output/snippets.js +0 -107
  55. package/core/server/api/canary/utils/serializers/output/webhooks.js +0 -15
@@ -0,0 +1,20 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('newsletters', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ name: {type: 'string', maxlength: 191, nullable: false},
6
+ description: {type: 'string', maxlength: 2000, nullable: true},
7
+ sender_name: {type: 'string', maxlength: 191, nullable: false},
8
+ sender_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
9
+ sender_reply_to: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
10
+ default: {type: 'bool', nullable: false, defaultTo: false},
11
+ status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
12
+ recipient_filter: {
13
+ type: 'text',
14
+ maxlength: 1000000000,
15
+ nullable: false,
16
+ defaultTo: ''
17
+ },
18
+ subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: false},
19
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
20
+ });
@@ -0,0 +1,28 @@
1
+ const {
2
+ addPermissionWithRoles,
3
+ combineTransactionalMigrations
4
+ } = require('../../utils');
5
+
6
+ module.exports = combineTransactionalMigrations(
7
+ addPermissionWithRoles({
8
+ name: 'Browse newsletters',
9
+ action: 'browse',
10
+ object: 'newsletter'
11
+ }, [
12
+ 'Administrator'
13
+ ]),
14
+ addPermissionWithRoles({
15
+ name: 'Add newsletters',
16
+ action: 'add',
17
+ object: 'newsletter'
18
+ }, [
19
+ 'Administrator'
20
+ ]),
21
+ addPermissionWithRoles({
22
+ name: 'Edit newsletters',
23
+ action: 'edit',
24
+ object: 'newsletter'
25
+ }, [
26
+ 'Administrator'
27
+ ])
28
+ );
@@ -80,18 +80,18 @@ function dropColumn(tableName, column, transaction = db.knex) {
80
80
  */
81
81
  async function addUnique(tableName, columns, transaction = db.knex) {
82
82
  try {
83
- logging.info(`Adding unique constraint for: ${columns} in table ${tableName}`);
83
+ logging.info(`Adding unique constraint for '${columns}' in table '${tableName}'`);
84
84
 
85
85
  return await transaction.schema.table(tableName, function (table) {
86
86
  table.unique(columns);
87
87
  });
88
88
  } catch (err) {
89
89
  if (err.code === 'SQLITE_ERROR') {
90
- logging.warn(`Constraint for: ${columns} already exists for table: ${tableName}`);
90
+ logging.warn(`Constraint for '${columns}' already exists for table '${tableName}'`);
91
91
  return;
92
92
  }
93
93
  if (err.code === 'ER_DUP_KEYNAME') {
94
- logging.warn(`Constraint for: ${columns} already exists for table: ${tableName}`);
94
+ logging.warn(`Constraint for '${columns}' already exists for table '${tableName}'`);
95
95
  return;
96
96
  }
97
97
  throw err;
@@ -107,18 +107,18 @@ async function addUnique(tableName, columns, transaction = db.knex) {
107
107
  */
108
108
  async function dropUnique(tableName, columns, transaction = db.knex) {
109
109
  try {
110
- logging.info(`Dropping unique constraint for: ${columns} in table: ${tableName}`);
110
+ logging.info(`Dropping unique constraint for '${columns}' in table '${tableName}'`);
111
111
 
112
112
  return await transaction.schema.table(tableName, function (table) {
113
113
  table.dropUnique(columns);
114
114
  });
115
115
  } catch (err) {
116
116
  if (err.code === 'SQLITE_ERROR') {
117
- logging.warn(`Constraint for: ${columns} does not exist for table: ${tableName}`);
117
+ logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`);
118
118
  return;
119
119
  }
120
120
  if (err.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
121
- logging.warn(`Constraint for: ${columns} does not exist for table: ${tableName}`);
121
+ logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`);
122
122
  return;
123
123
  }
124
124
  throw err;
@@ -164,7 +164,7 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
164
164
  if (DatabaseInfo.isSQLite(transaction)) {
165
165
  const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
166
166
  if (foreignKeyExists) {
167
- logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key already exists`);
167
+ logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - already exists`);
168
168
  return;
169
169
  }
170
170
  }
@@ -195,7 +195,7 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
195
195
  }
196
196
  } catch (err) {
197
197
  if (err.code === 'ER_DUP_KEY' || err.code === 'ER_FK_DUP_KEY' || err.code === 'ER_FK_DUP_NAME') {
198
- logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key already exists`);
198
+ logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - already exists`);
199
199
  return;
200
200
  }
201
201
  throw err;
@@ -216,7 +216,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
216
216
  if (DatabaseInfo.isSQLite(transaction)) {
217
217
  const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
218
218
  if (!foreignKeyExists) {
219
- logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key does not exist`);
219
+ logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - does not exist`);
220
220
  return;
221
221
  }
222
222
  }
@@ -243,7 +243,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
243
243
  }
244
244
  } catch (err) {
245
245
  if (err.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
246
- logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key does not exist`);
246
+ logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - does not exist`);
247
247
  return;
248
248
  }
249
249
  throw err;
@@ -280,18 +280,18 @@ async function addPrimaryKey(tableName, columns, transaction = db.knex) {
280
280
  if (DatabaseInfo.isSQLite(transaction)) {
281
281
  const primaryKeyExists = await hasPrimaryKeySQLite(tableName, transaction);
282
282
  if (primaryKeyExists) {
283
- logging.warn(`Primary key constraint for: ${columns} already exists for table: ${tableName}`);
283
+ logging.warn(`Primary key constraint for '${columns}' already exists for table '${tableName}'`);
284
284
  return;
285
285
  }
286
286
  }
287
287
  try {
288
- logging.info(`Adding primary key constraint for: ${columns} in table ${tableName}`);
288
+ logging.info(`Adding primary key constraint for '${columns}' in table '${tableName}'`);
289
289
  return await transaction.schema.table(tableName, function (table) {
290
290
  table.primary(columns);
291
291
  });
292
292
  } catch (err) {
293
293
  if (err.code === 'ER_MULTIPLE_PRI_KEY') {
294
- logging.warn(`Primary key constraint for: ${columns} already exists for table: ${tableName}`);
294
+ logging.warn(`Primary key constraint for '${columns}' already exists for table '${tableName}'`);
295
295
  return;
296
296
  }
297
297
  throw err;
@@ -711,5 +711,23 @@ module.exports = {
711
711
  }
712
712
  },
713
713
  value: {type: 'text', maxlength: 65535, nullable: true}
714
+ },
715
+ newsletters: {
716
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
717
+ name: {type: 'string', maxlength: 191, nullable: false},
718
+ description: {type: 'string', maxlength: 2000, nullable: true},
719
+ sender_name: {type: 'string', maxlength: 191, nullable: false},
720
+ sender_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
721
+ sender_reply_to: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
722
+ default: {type: 'bool', nullable: false, defaultTo: false},
723
+ status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
724
+ recipient_filter: {
725
+ type: 'text',
726
+ maxlength: 1000000000,
727
+ nullable: false,
728
+ defaultTo: ''
729
+ },
730
+ subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: false},
731
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
714
732
  }
715
733
  };
@@ -0,0 +1,9 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const Newsletter = ghostBookshelf.Model.extend({
4
+ tableName: 'newsletters'
5
+ });
6
+
7
+ module.exports = {
8
+ Newsletter: ghostBookshelf.model('Newsletter', Newsletter)
9
+ };
@@ -198,48 +198,55 @@ h5,
198
198
  h6 {
199
199
  margin-top: 0;
200
200
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
201
- line-height: 1.15em;
202
- font-weight: 600;
201
+ line-height: 1.11em;
202
+ font-weight: 700;
203
203
  text-rendering: optimizeLegibility;
204
204
  }
205
205
 
206
206
  h1 {
207
207
  margin: 1.5em 0 0.5em 0;
208
208
  font-size: 42px;
209
- font-weight: 600;
209
+ font-weight: 700;
210
210
  }
211
211
 
212
212
  h2 {
213
213
  margin: 1.5em 0 0.5em 0;
214
214
  font-size: 32px;
215
- line-height: 1.22em;
216
215
  }
217
216
 
218
217
  h3 {
219
218
  margin: 1.5em 0 0.5em 0;
220
219
  font-size: 26px;
221
- line-height: 1.25em;
222
220
  }
223
221
 
224
222
  h4 {
225
223
  margin: 1.8em 0 0.5em 0;
226
224
  font-size: 21px;
227
- line-height: 1.3em;
225
+ line-height: 1.2em;
228
226
  }
229
227
 
230
228
  h5 {
231
229
  margin: 2em 0 0.5em 0;
232
230
  font-size: 19px;
233
- line-height: 1.4em;
231
+ line-height: 1.3em;
234
232
  }
235
233
 
236
234
  h6 {
237
235
  margin: 2em 0 0.5em 0;
238
236
  font-size: 19px;
239
- line-height: 1.4em;
237
+ line-height: 1.3em;
240
238
  font-weight: 700;
241
239
  }
242
240
 
241
+ h1 strong,
242
+ h2 strong,
243
+ h3 strong,
244
+ h4 strong,
245
+ h5 strong,
246
+ h6 strong {
247
+ font-weight: 800;
248
+ }
249
+
243
250
  strong {
244
251
  font-weight: 700;
245
252
  }
@@ -301,6 +308,10 @@ figure blockquote p {
301
308
 
302
309
  .site-info {
303
310
  padding-top: 50px;
311
+ }
312
+
313
+ .site-info-bordered {
314
+ padding-top: 50px;
304
315
  border-bottom: 1px solid #e5eff5;
305
316
  }
306
317
 
@@ -322,11 +333,12 @@ figure blockquote p {
322
333
  padding-bottom: 10px;
323
334
  font-size: 42px;
324
335
  line-height: 1.1em;
325
- font-weight: 600;
336
+ font-weight: 700;
326
337
  text-align: center;
327
338
  }
328
339
  .post-title-serif {
329
340
  font-family: Georgia, serif;
341
+ letter-spacing: -0.01em;
330
342
  }
331
343
  .post-title-left {
332
344
  text-align: left;
@@ -388,7 +400,7 @@ figure blockquote p {
388
400
  font-family: Georgia, serif;
389
401
  font-size: 18px;
390
402
  line-height: 1.5em;
391
- color: #23323D;
403
+ color: #15212A;
392
404
  padding-bottom: 20px;
393
405
  border-bottom: 1px solid #e5eff5;
394
406
  }
@@ -397,7 +409,7 @@ figure blockquote p {
397
409
  max-width: 600px !important;
398
410
  font-size: 17px;
399
411
  line-height: 1.5em;
400
- color: #23323D;
412
+ color: #15212A;
401
413
  padding-bottom: 20px;
402
414
  border-bottom: 1px solid #e5eff5;
403
415
  }
@@ -711,7 +723,7 @@ a[data-flickr-embed] img {
711
723
  }
712
724
 
713
725
  .kg-header-card h3 strong {
714
- font-weight: 600;
726
+ font-weight: 700;
715
727
  }
716
728
 
717
729
  .kg-header-card.kg-size-large h3 {
@@ -1148,7 +1160,7 @@ ${ templateSettings.showBadge ? `
1148
1160
 
1149
1161
  ${ templateSettings.showHeaderIcon || templateSettings.showHeaderTitle ? `
1150
1162
  <tr>
1151
- <td class="site-info" width="100%" align="center">
1163
+ <td class="${templateSettings.showHeaderTitle ? `site-info-bordered` : `site-info`}" width="100%" align="center">
1152
1164
  <table role="presentation" border="0" cellpadding="0" cellspacing="0">
1153
1165
  ${ templateSettings.showHeaderIcon && site.iconUrl ? `
1154
1166
  <tr>
@@ -19,6 +19,7 @@ const VerificationTrigger = require('@tryghost/verification-trigger');
19
19
  const DomainEvents = require('@tryghost/domain-events');
20
20
  const {LastSeenAtUpdater} = require('@tryghost/members-events-service');
21
21
  const events = require('../../lib/common/events');
22
+ const DatabaseInfo = require('@tryghost/database-info');
22
23
 
23
24
  const messages = {
24
25
  noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
@@ -37,7 +38,7 @@ const membersConfig = new MembersConfigProvider({
37
38
  const membersStats = new MembersStats({
38
39
  db: db,
39
40
  settingsCache: settingsCache,
40
- isSQLite: config.get('database:client') === 'sqlite3'
41
+ isSQLite: DatabaseInfo.isSQLite(db.knex)
41
42
  });
42
43
 
43
44
  let membersApi;
@@ -0,0 +1 @@
1
+ module.exports = require('./service');
@@ -0,0 +1,165 @@
1
+ const {DateTime} = require('luxon');
2
+
3
+ class MembersStatsService {
4
+ constructor({db}) {
5
+ this.db = db;
6
+ }
7
+
8
+ /**
9
+ * Get the current total members grouped by status
10
+ * @returns {Promise<TotalMembersByStatus>}
11
+ */
12
+ async getCount() {
13
+ const knex = this.db.knex;
14
+ const rows = await knex('members')
15
+ .select('status')
16
+ .select(knex.raw('COUNT(id) AS total'))
17
+ .groupBy('status');
18
+
19
+ const paidEvent = rows.find(c => c.status === 'paid');
20
+ const freeEvent = rows.find(c => c.status === 'free');
21
+ const compedEvent = rows.find(c => c.status === 'comped');
22
+
23
+ return {
24
+ paid: paidEvent ? paidEvent.total : 0,
25
+ free: freeEvent ? freeEvent.total : 0,
26
+ comped: compedEvent ? compedEvent.total : 0
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Get the member deltas by status for all days (from new to old)
32
+ * @returns {Promise<MemberStatusDelta[]>} The deltas of paid, free and comped users per day, sorted from new to old
33
+ */
34
+ async fetchAllStatusDeltas() {
35
+ const knex = this.db.knex;
36
+ const rows = await knex('members_status_events')
37
+ .select(knex.raw('DATE(created_at) as date'))
38
+ .select(knex.raw(`SUM(
39
+ CASE WHEN to_status='paid' THEN 1
40
+ ELSE 0 END
41
+ ) as paid_subscribed`))
42
+ .select(knex.raw(`SUM(
43
+ CASE WHEN from_status='paid' THEN 1
44
+ ELSE 0 END
45
+ ) as paid_canceled`))
46
+ .select(knex.raw(`SUM(
47
+ CASE WHEN to_status='comped' THEN 1
48
+ WHEN from_status='comped' THEN -1
49
+ ELSE 0 END
50
+ ) as comped_delta`))
51
+ .select(knex.raw(`SUM(
52
+ CASE WHEN to_status='free' THEN 1
53
+ WHEN from_status='free' THEN -1
54
+ ELSE 0 END
55
+ ) as free_delta`))
56
+ .groupByRaw('DATE(created_at)')
57
+ .orderByRaw('DATE(created_at) DESC');
58
+ return rows;
59
+ }
60
+
61
+ /**
62
+ * Returns a list of the total members by status for each day, including the paid deltas paid_subscribed and paid_canceled
63
+ * @returns {Promise<CountHistory>}
64
+ */
65
+ async getCountHistory() {
66
+ const rows = await this.fetchAllStatusDeltas();
67
+
68
+ // Fetch current total amounts and start counting from there
69
+ const totals = await this.getCount();
70
+ let {paid, free, comped} = totals;
71
+
72
+ // Get today in UTC (default timezone for Luxon)
73
+ const today = DateTime.local().toISODate();
74
+
75
+ const cumulativeResults = [];
76
+ for (const row of rows) {
77
+ // Convert JSDates to YYYY-MM-DD (in UTC)
78
+ const date = DateTime.fromJSDate(row.date).toISODate();
79
+ if (date > today) {
80
+ // Skip results that are in the future (fix for invalid events)
81
+ continue;
82
+ }
83
+ cumulativeResults.unshift({
84
+ date,
85
+ paid,
86
+ free,
87
+ comped,
88
+
89
+ // Deltas
90
+ paid_subscribed: row.paid_subscribed,
91
+ paid_canceled: row.paid_canceled
92
+ });
93
+
94
+ // Update current counts
95
+ paid = Math.max(0, paid - row.paid_subscribed + row.paid_canceled);
96
+ free = Math.max(0, free - row.free_delta);
97
+ comped = Math.max(0, comped - row.comped_delta);
98
+ }
99
+
100
+ // Always make sure we have at least one result
101
+ if (cumulativeResults.length === 0) {
102
+ cumulativeResults.push({
103
+ date: today,
104
+ paid,
105
+ free,
106
+ comped,
107
+
108
+ // Deltas
109
+ paid_subscribed: 0,
110
+ paid_canceled: 0
111
+ });
112
+ }
113
+
114
+ return {
115
+ data: cumulativeResults,
116
+ meta: {
117
+ pagination: {
118
+ page: 1,
119
+ limit: 'all',
120
+ pages: 1,
121
+ total: cumulativeResults.length,
122
+ next: null,
123
+ prev: null
124
+ },
125
+ totals
126
+ }
127
+ };
128
+ }
129
+ }
130
+
131
+ module.exports = MembersStatsService;
132
+
133
+ /**
134
+ * @typedef MemberStatusDelta
135
+ * @type {Object}
136
+ * @property {Date} date
137
+ * @property {number} paid_subscribed Paid members that subscribed on this day
138
+ * @property {number} paid_canceled Paid members that canceled on this day
139
+ * @property {number} comped_delta Total net comped members on this day
140
+ * @property {number} free_delta Total net members on this day
141
+ */
142
+
143
+ /**
144
+ * @typedef TotalMembersByStatus
145
+ * @type {Object}
146
+ * @property {number} paid Total paid members
147
+ * @property {number} free Total free members
148
+ * @property {number} comped Total comped members
149
+ */
150
+
151
+ /**
152
+ * @typedef {Object} TotalMembersByStatusItem
153
+ * @property {string} date In YYYY-MM-DD format
154
+ * @property {number} paid Total paid members
155
+ * @property {number} free Total free members
156
+ * @property {number} comped Total comped members
157
+ * @property {number} paid_subscribed Paid members that subscribed on this day
158
+ * @property {number} paid_canceled Paid members that canceled on this day
159
+ */
160
+
161
+ /**
162
+ * @typedef {Object} CountHistory
163
+ * @property {TotalMembersByStatusItem[]} data List of the total members by status for each day, including the paid deltas paid_subscribed and paid_canceled
164
+ * @property {Object} meta
165
+ */
@@ -0,0 +1,6 @@
1
+ const db = require('../../data/db');
2
+ const MemberStatsService = require('./lib/members-stats-service');
3
+
4
+ module.exports = {
5
+ members: new MemberStatsService({db})
6
+ };
@@ -32,7 +32,9 @@ class WebhooksService {
32
32
  const newWebhook = await this.WebhookModel.add(data.webhooks[0], options);
33
33
  return newWebhook;
34
34
  } catch (error) {
35
- if (error.errno === 1452 || (error.code === 'SQLITE_CONSTRAINT' && /SQLITE_CONSTRAINT: FOREIGN KEY constraint failed/.test(error.message))) {
35
+ if (error.errno === 1452
36
+ || (error.code === 'SQLITE_CONSTRAINT' && /SQLITE_CONSTRAINT: FOREIGN KEY constraint failed/.test(error.message))
37
+ || (error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY')) {
36
38
  throw new ValidationError({
37
39
  message: tpl(messages.nonExistingIntegrationIdProvided.message, {
38
40
  key: 'integration_id'
@@ -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.41%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.42%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" />
@@ -37,8 +37,8 @@
37
37
  </style>
38
38
 
39
39
 
40
- <link rel="stylesheet" href="assets/vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css">
41
- <link rel="stylesheet" href="assets/ghost.min-a73b150c7eecc4641d377cc73fb5eecd.css" title="light">
40
+ <link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
41
+ <link rel="stylesheet" href="assets/ghost.min-c08ce1872f0e09edb63eb13c43606d18.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-9094db77ba3190cb10876f8e42e1d90d.js"></script>
60
- <script src="assets/ghost.min-1abf114ca26a71e8e1f09054f3592614.js"></script>
59
+ <script src="assets/vendor.min-21f79c68a284acb1b70039f3f63e5507.js"></script>
60
+ <script src="assets/ghost.min-20096eef632760c3a2906e243adbd24b.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.41%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.42%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" />
@@ -37,8 +37,8 @@
37
37
  </style>
38
38
 
39
39
 
40
- <link rel="stylesheet" href="assets/vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css">
41
- <link rel="stylesheet" href="assets/ghost.min-a73b150c7eecc4641d377cc73fb5eecd.css" title="light">
40
+ <link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
41
+ <link rel="stylesheet" href="assets/ghost.min-c08ce1872f0e09edb63eb13c43606d18.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-9094db77ba3190cb10876f8e42e1d90d.js"></script>
60
- <script src="assets/ghost.min-1abf114ca26a71e8e1f09054f3592614.js"></script>
59
+ <script src="assets/vendor.min-21f79c68a284acb1b70039f3f63e5507.js"></script>
60
+ <script src="assets/ghost.min-20096eef632760c3a2906e243adbd24b.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -113,8 +113,6 @@ module.exports = function apiRoutes() {
113
113
 
114
114
  router.get('/members/stats/count', mw.authAdminApi, http(api.members.memberStats));
115
115
  router.get('/members/stats/mrr', mw.authAdminApi, http(api.members.mrrStats));
116
- router.get('/members/stats/subscribers', mw.authAdminApi, http(api.members.subscriberStats));
117
- router.get('/members/stats/gross_volume', mw.authAdminApi, http(api.members.grossVolumeStats));
118
116
 
119
117
  router.get('/members/events', mw.authAdminApi, http(api.members.activityFeed));
120
118
 
@@ -139,6 +137,9 @@ module.exports = function apiRoutes() {
139
137
 
140
138
  router.get('/members/:id/signin_urls', mw.authAdminApi, http(api.memberSigninUrls.read));
141
139
 
140
+ // ## Stats
141
+ router.get('/stats/member_count', mw.authAdminApi, http(api.stats.memberCountHistory));
142
+
142
143
  // ## Labels
143
144
  router.get('/labels', mw.authAdminApi, http(api.labels.browse));
144
145
  router.get('/labels/:id', mw.authAdminApi, http(api.labels.read));
@@ -290,6 +291,7 @@ module.exports = function apiRoutes() {
290
291
  router.get('/actions', mw.authAdminApi, http(api.actions.browse));
291
292
 
292
293
  // ## Email Preview
294
+ // @TODO: rename to email_previews in 5.0
293
295
  router.get('/email_preview/posts/:id', mw.authAdminApi, http(api.email_preview.read));
294
296
  router.post('/email_preview/posts/:id', mw.authAdminApi, http(api.email_preview.sendTestEmail));
295
297
 
@@ -309,5 +311,9 @@ module.exports = function apiRoutes() {
309
311
  router.get('/custom_theme_settings', mw.authAdminApi, http(api.customThemeSettings.browse));
310
312
  router.put('/custom_theme_settings', mw.authAdminApi, http(api.customThemeSettings.edit));
311
313
 
314
+ router.get('/newsletters', mw.authAdminApi, http(api.newsletters.browse));
315
+ router.post('/newsletters', mw.authAdminApi, http(api.newsletters.add));
316
+ router.put('/newsletters/:id', mw.authAdminApi, http(api.newsletters.edit));
317
+
312
318
  return router;
313
319
  };
@@ -128,8 +128,8 @@
128
128
  "emailAnalytics": true
129
129
  },
130
130
  "portal": {
131
- "url": "https://unpkg.com/@tryghost/portal@~1.17.0/umd/portal.min.js",
132
- "version": "1.17"
131
+ "url": "https://unpkg.com/@tryghost/portal@~1.18.0/umd/portal.min.js",
132
+ "version": "1.18"
133
133
  },
134
134
  "tenor": {
135
135
  "publicReadOnlyApiKey": null,
@@ -0,0 +1,26 @@
1
+ {
2
+ "url": "http://localhost:2368",
3
+ "database": {
4
+ "client": "sqlite3",
5
+ "connection": {
6
+ "filename": "content/data/ghost-dev.db"
7
+ },
8
+ "debug": false
9
+ },
10
+ "paths": {
11
+ "contentPath": "content/"
12
+ },
13
+ "privacy": {
14
+ "useRpcPing": false,
15
+ "useUpdateCheck": true
16
+ },
17
+ "useMinFiles": false,
18
+ "caching": {
19
+ "theme": {
20
+ "maxAge": 0
21
+ },
22
+ "admin": {
23
+ "maxAge": 0
24
+ }
25
+ }
26
+ }