ghost 4.41.2 → 4.42.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 (82) hide show
  1. package/core/boot.js +24 -6
  2. package/core/built/assets/ghost-dark-a93afb20027060d760ac6d78f115a76f.css +1 -0
  3. package/core/built/assets/{ghost.min-8e2e6c7a01fde044c566c1650a36bfc2.js → ghost.min-20096eef632760c3a2906e243adbd24b.js} +1035 -841
  4. package/core/built/assets/ghost.min-ce35ef1b76d9a943ab912c076773b132.css +1 -0
  5. package/core/built/assets/{vendor.min-9094db77ba3190cb10876f8e42e1d90d.js → vendor.min-21f79c68a284acb1b70039f3f63e5507.js} +68 -68
  6. package/core/built/assets/{vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css → vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css} +214 -0
  7. package/core/frontend/helpers/price.js +1 -0
  8. package/core/frontend/web/middleware/error-handler.js +5 -3
  9. package/core/server/api/canary/email-preview.js +2 -1
  10. package/core/server/api/canary/{email.js → emails.js} +0 -0
  11. package/core/server/api/canary/index.js +11 -3
  12. package/core/server/api/canary/{memberSigninUrls.js → member-signin-urls.js} +0 -1
  13. package/core/server/api/canary/{membersStripeConnect.js → members-stripe-connect.js} +0 -0
  14. package/core/server/api/canary/members.js +0 -45
  15. package/core/server/api/canary/newsletters.js +45 -0
  16. package/core/server/api/canary/stats.js +14 -0
  17. package/core/server/api/canary/utils/serializers/output/authentication.js +4 -0
  18. package/core/server/api/canary/utils/serializers/output/default.js +35 -0
  19. package/core/server/api/canary/utils/serializers/output/email-previews.js +7 -0
  20. package/core/server/api/canary/utils/serializers/output/index.js +18 -42
  21. package/core/server/api/canary/utils/serializers/output/mappers/authors.js +1 -0
  22. package/core/server/api/canary/utils/serializers/output/mappers/index.js +2 -1
  23. package/core/server/api/canary/utils/serializers/output/mappers/integrations.js +1 -1
  24. package/core/server/api/canary/utils/serializers/output/mappers/snippets.js +36 -0
  25. package/core/server/api/canary/utils/serializers/output/members-stripe-connect.js +6 -0
  26. package/core/server/api/canary/utils/serializers/output/members.js +2 -2
  27. package/core/server/api/canary/utils/serializers/output/oembed.js +2 -2
  28. package/core/server/api/canary/utils/serializers/output/offers.js +8 -0
  29. package/core/server/api/canary/utils/serializers/output/redirects.js +2 -2
  30. package/core/server/api/canary/utils/serializers/output/schedules.js +2 -2
  31. package/core/server/api/canary/utils/serializers/output/session.js +9 -0
  32. package/core/server/api/canary/utils/serializers/output/settings.js +64 -37
  33. package/core/server/api/canary/utils/serializers/output/slack.js +9 -0
  34. package/core/server/api/canary/utils/serializers/output/themes.js +3 -24
  35. package/core/server/api/canary/utils/serializers/output/users.js +0 -23
  36. package/core/server/api/shared/serializers/handle.js +25 -11
  37. package/core/server/data/exporter/table-lists.js +1 -0
  38. package/core/server/data/migrations/utils.js +1 -1
  39. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +20 -0
  40. package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js +28 -0
  41. package/core/server/data/schema/commands.js +13 -13
  42. package/core/server/data/schema/schema.js +18 -0
  43. package/core/server/models/newsletter.js +9 -0
  44. package/core/server/services/mega/template.js +25 -13
  45. package/core/server/services/members/service.js +2 -1
  46. package/core/server/services/offers/service.js +11 -8
  47. package/core/server/services/stats/index.js +1 -0
  48. package/core/server/services/stats/lib/members-stats-service.js +165 -0
  49. package/core/server/services/stats/service.js +6 -0
  50. package/core/server/services/themes/validate.js +4 -3
  51. package/core/server/services/webhooks/webhooks-service.js +3 -1
  52. package/core/server/web/admin/app.js +8 -0
  53. package/core/server/web/admin/views/default-prod.html +5 -5
  54. package/core/server/web/admin/views/default.html +5 -5
  55. package/core/server/web/api/canary/admin/routes.js +8 -2
  56. package/core/shared/config/defaults.json +2 -2
  57. package/core/shared/config/env/config.development.json +26 -0
  58. package/core/shared/config/env/config.production.json +21 -0
  59. package/core/shared/config/env/config.testing-mysql.json +59 -0
  60. package/core/shared/config/env/config.testing.json +58 -0
  61. package/core/shared/labs.js +3 -1
  62. package/core/shared/settings-cache/cache.js +1 -1
  63. package/package.json +62 -62
  64. package/yarn.lock +1586 -1757
  65. package/.c8rc.json +0 -34
  66. package/.eslintrc.js +0 -118
  67. package/core/built/assets/ghost-dark-6fbe502f2bb2cde92e15b2f1a9da57a0.css +0 -1
  68. package/core/built/assets/ghost.min-09301e5bd933cf6d24368e98a4d898a9.css +0 -1
  69. package/core/server/api/canary/utils/serializers/output/actions.js +0 -13
  70. package/core/server/api/canary/utils/serializers/output/authors.js +0 -21
  71. package/core/server/api/canary/utils/serializers/output/email-preview.js +0 -7
  72. package/core/server/api/canary/utils/serializers/output/emails.js +0 -22
  73. package/core/server/api/canary/utils/serializers/output/identities.js +0 -7
  74. package/core/server/api/canary/utils/serializers/output/integrations.js +0 -34
  75. package/core/server/api/canary/utils/serializers/output/invites.js +0 -24
  76. package/core/server/api/canary/utils/serializers/output/labels.js +0 -25
  77. package/core/server/api/canary/utils/serializers/output/mappers/labels.js +0 -4
  78. package/core/server/api/canary/utils/serializers/output/member-signin_urls.js +0 -7
  79. package/core/server/api/canary/utils/serializers/output/snippets.js +0 -97
  80. package/core/server/api/canary/utils/serializers/output/tags.js +0 -25
  81. package/core/server/api/canary/utils/serializers/output/webhooks.js +0 -15
  82. package/jsconfig.json +0 -13
@@ -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;
@@ -5,19 +5,20 @@ const config = require('../../../shared/config');
5
5
  const urlUtils = require('../../../shared/url-utils');
6
6
  const models = require('../../models');
7
7
 
8
- const redirectManager = new DynamicRedirectManager({
9
- permanentMaxAge: config.get('caching:customRedirects:maxAge'),
10
- getSubdirectoryURL: (pathname) => {
11
- return urlUtils.urlJoin(urlUtils.getSubdir(), pathname);
12
- }
13
- });
8
+ let redirectManager;
14
9
 
15
10
  module.exports = {
16
11
  async init() {
12
+ redirectManager = new DynamicRedirectManager({
13
+ permanentMaxAge: config.get('caching:customRedirects:maxAge'),
14
+ getSubdirectoryURL: (pathname) => {
15
+ return urlUtils.urlJoin(urlUtils.getSubdir(), pathname);
16
+ }
17
+ });
17
18
  const offersModule = OffersModule.create({
18
19
  OfferModel: models.Offer,
19
20
  OfferRedemptionModel: models.OfferRedemption,
20
- redirectManager: redirectManager
21
+ redirectManager
21
22
  });
22
23
 
23
24
  this.api = offersModule.api;
@@ -27,5 +28,7 @@ module.exports = {
27
28
 
28
29
  api: null,
29
30
 
30
- middleware: redirectManager.handleRequest
31
+ get middleware() {
32
+ return redirectManager.handleRequest;
33
+ }
31
34
  };
@@ -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
+ };
@@ -22,26 +22,27 @@ const check = async function check(theme, isZip) {
22
22
  debug('Begin: Check');
23
23
  // gscan can slow down boot time if we require on boot, for now nest the require.
24
24
  const gscan = require('gscan');
25
+ const checkedVersion = 'v4';
25
26
  let checkedTheme;
26
27
 
27
28
  if (isZip) {
28
29
  debug('zip mode');
29
30
  checkedTheme = await gscan.checkZip(theme, {
30
31
  keepExtractedDir: true,
31
- checkVersion: 'v4',
32
+ checkVersion: checkedVersion,
32
33
  labs: labs.getAll()
33
34
  });
34
35
  } else {
35
36
  debug('non-zip mode');
36
37
  checkedTheme = await gscan.check(theme.path, {
37
- checkVersion: 'v4',
38
+ checkVersion: checkedVersion,
38
39
  labs: labs.getAll()
39
40
  });
40
41
  }
41
42
 
42
43
  checkedTheme = gscan.format(checkedTheme, {
43
44
  onlyFatalErrors: config.get('env') === 'production',
44
- checkVersion: 'v4'
45
+ checkVersion: checkedVersion
45
46
  });
46
47
 
47
48
  debug('End: Check');
@@ -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'
@@ -46,6 +46,14 @@ module.exports = function setupAdminApp() {
46
46
  // Finally, routing
47
47
  adminApp.get('*', require('./controller'));
48
48
 
49
+ adminApp.use((err, req, res, next) => {
50
+ if (err.statusCode && err.statusCode === 404) {
51
+ // Remove 404 errors for next middleware to inject
52
+ next();
53
+ } else {
54
+ next(err);
55
+ }
56
+ });
49
57
  adminApp.use(errorHandler.pageNotFound);
50
58
  adminApp.use(errorHandler.handleHTMLResponse(sentry));
51
59
 
@@ -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-09301e5bd933cf6d24368e98a4d898a9.css" title="light">
40
+ <link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
41
+ <link rel="stylesheet" href="assets/ghost.min-ce35ef1b76d9a943ab912c076773b132.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-8e2e6c7a01fde044c566c1650a36bfc2.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-09301e5bd933cf6d24368e98a4d898a9.css" title="light">
40
+ <link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
41
+ <link rel="stylesheet" href="assets/ghost.min-ce35ef1b76d9a943ab912c076773b132.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-8e2e6c7a01fde044c566c1650a36bfc2.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
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "database": {
3
+ "client": "mysql",
4
+ "connection": {
5
+ "host" : "127.0.0.1",
6
+ "user" : "root",
7
+ "password" : "",
8
+ "database" : "ghost"
9
+ }
10
+ },
11
+ "paths": {
12
+ "contentPath": "content/"
13
+ },
14
+ "logging": {
15
+ "level": "info",
16
+ "rotation": {
17
+ "enabled": true
18
+ },
19
+ "transports": ["file", "stdout"]
20
+ }
21
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "url": "http://127.0.0.1:2369",
3
+ "server": {
4
+ "port": 2369
5
+ },
6
+ "database": {
7
+ "client": "mysql",
8
+ "connection": {
9
+ "host" : "127.0.0.1",
10
+ "user" : "root",
11
+ "password" : "",
12
+ "database" : "ghost_testing"
13
+ }
14
+ },
15
+ "logging": {
16
+ "level": "fatal"
17
+ },
18
+ "spam": {
19
+ "user_login": {
20
+ "minWait": 600000,
21
+ "maxWait": 604800000,
22
+ "freeRetries": 3
23
+ },
24
+ "user_reset": {
25
+ "minWait": 3600000,
26
+ "maxWait": 3600000,
27
+ "lifetime": 3600,
28
+ "freeRetries": 4
29
+ },
30
+ "global_reset": {
31
+ "minWait": 3600000,
32
+ "maxWait": 3600000,
33
+ "lifetime": 3600,
34
+ "freeRetries":4
35
+ },
36
+ "global_block": {
37
+ "minWait": 3600000,
38
+ "maxWait": 3600000,
39
+ "lifetime": 3600,
40
+ "freeRetries":4
41
+ },
42
+ "private_block": {
43
+ "minWait": 3600000,
44
+ "maxWait": 3600000,
45
+ "lifetime": 3600,
46
+ "freeRetries":99
47
+ }
48
+ },
49
+ "privacy": {
50
+ "useTinfoil": true,
51
+ "useStructuredData": true
52
+ },
53
+ "useMinFiles": false,
54
+ "paths": {
55
+ "fixtures": "test/utils/fixtures/fixtures",
56
+ "defaultSettings": "test/utils/fixtures/default-settings.json",
57
+ "urlCache": "test/utils/fixtures/urls"
58
+ }
59
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "url": "http://127.0.0.1:2369",
3
+ "database": {
4
+ "client": "sqlite3",
5
+ "connection": {
6
+ "filename": "/tmp/ghost-test.db"
7
+ },
8
+ "useNullAsDefault": true,
9
+ "debug": false
10
+ },
11
+ "server": {
12
+ "port": 2369
13
+ },
14
+ "logging": {
15
+ "level": "fatal"
16
+ },
17
+ "spam": {
18
+ "user_login": {
19
+ "minWait": 600000,
20
+ "maxWait": 604800000,
21
+ "freeRetries": 3
22
+ },
23
+ "user_reset": {
24
+ "minWait": 3600000,
25
+ "maxWait": 3600000,
26
+ "lifetime": 3600,
27
+ "freeRetries": 4
28
+ },
29
+ "global_reset": {
30
+ "minWait": 3600000,
31
+ "maxWait": 3600000,
32
+ "lifetime": 3600,
33
+ "freeRetries":4
34
+ },
35
+ "global_block": {
36
+ "minWait": 3600000,
37
+ "maxWait": 3600000,
38
+ "lifetime": 3600,
39
+ "freeRetries":4
40
+ },
41
+ "private_block": {
42
+ "minWait": 3600000,
43
+ "maxWait": 3600000,
44
+ "lifetime": 3600,
45
+ "freeRetries":99
46
+ }
47
+ },
48
+ "privacy": {
49
+ "useTinfoil": true,
50
+ "useStructuredData": true
51
+ },
52
+ "useMinFiles": false,
53
+ "paths": {
54
+ "fixtures": "test/utils/fixtures/fixtures",
55
+ "defaultSettings": "test/utils/fixtures/default-settings.json",
56
+ "urlCache": "test/utils/fixtures/urls"
57
+ }
58
+ }
@@ -35,7 +35,9 @@ const ALPHA_FEATURES = [
35
35
  'membersActivity',
36
36
  'urlCache',
37
37
  'beforeAfterCard',
38
- 'tweetGridCard'
38
+ 'tweetGridCard',
39
+ 'multipleNewsletters',
40
+ 'dashboardV5'
39
41
  ];
40
42
 
41
43
  module.exports.GA_KEYS = [...GA_FEATURES];
@@ -28,7 +28,7 @@ const doGet = (key, options) => {
28
28
 
29
29
  // Don't try to resolve to the value of the setting
30
30
  if (options && options.resolve === false) {
31
- return settingsCache[key] || null;
31
+ return settingsCache[key];
32
32
  }
33
33
 
34
34
  // Default behaviour is to try to resolve the value and return that