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.
- package/core/built/assets/ghost-dark-97613c037232aba4490489431ce170ca.css +1 -0
- package/core/built/assets/{ghost.min-1abf114ca26a71e8e1f09054f3592614.js → ghost.min-20096eef632760c3a2906e243adbd24b.js} +400 -323
- package/core/built/assets/ghost.min-c08ce1872f0e09edb63eb13c43606d18.css +1 -0
- package/core/built/assets/{vendor.min-9094db77ba3190cb10876f8e42e1d90d.js → vendor.min-21f79c68a284acb1b70039f3f63e5507.js} +68 -68
- package/core/built/assets/{vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css → vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css} +214 -0
- package/core/frontend/helpers/price.js +1 -0
- package/core/server/api/canary/email-preview.js +2 -1
- package/core/server/api/canary/{email.js → emails.js} +0 -0
- package/core/server/api/canary/index.js +9 -1
- package/core/server/api/canary/members.js +0 -45
- package/core/server/api/canary/newsletters.js +45 -0
- package/core/server/api/canary/stats.js +14 -0
- package/core/server/api/canary/utils/serializers/output/email-previews.js +7 -0
- package/core/server/api/canary/utils/serializers/output/index.js +2 -22
- package/core/server/api/canary/utils/serializers/output/mappers/index.js +1 -0
- package/core/server/api/canary/utils/serializers/output/mappers/snippets.js +36 -0
- package/core/server/api/canary/utils/serializers/output/members.js +0 -2
- package/core/server/api/canary/utils/serializers/output/oembed.js +2 -2
- package/core/server/api/canary/utils/serializers/output/redirects.js +2 -2
- package/core/server/api/canary/utils/serializers/output/schedules.js +2 -2
- package/core/server/api/canary/utils/serializers/output/slack.js +2 -2
- package/core/server/api/canary/utils/serializers/output/themes.js +2 -2
- package/core/server/api/canary/utils/serializers/output/users.js +0 -23
- package/core/server/api/shared/serializers/handle.js +25 -26
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/migrations/utils.js +1 -1
- package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +20 -0
- package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js +28 -0
- package/core/server/data/schema/commands.js +13 -13
- package/core/server/data/schema/schema.js +18 -0
- package/core/server/models/newsletter.js +9 -0
- package/core/server/services/mega/template.js +25 -13
- package/core/server/services/members/service.js +2 -1
- package/core/server/services/stats/index.js +1 -0
- package/core/server/services/stats/lib/members-stats-service.js +165 -0
- package/core/server/services/stats/service.js +6 -0
- package/core/server/services/webhooks/webhooks-service.js +3 -1
- package/core/server/web/admin/views/default-prod.html +5 -5
- package/core/server/web/admin/views/default.html +5 -5
- package/core/server/web/api/canary/admin/routes.js +8 -2
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/env/config.development.json +26 -0
- package/core/shared/config/env/config.production.json +21 -0
- package/core/shared/config/env/config.testing-mysql.json +59 -0
- package/core/shared/config/env/config.testing.json +58 -0
- package/package.json +38 -38
- package/yarn.lock +510 -654
- package/core/built/assets/ghost-dark-146c4c688b47d45c4aa018ee0f79cebc.css +0 -1
- package/core/built/assets/ghost.min-a73b150c7eecc4641d377cc73fb5eecd.css +0 -1
- package/core/server/api/canary/utils/serializers/output/email-preview.js +0 -10
- package/core/server/api/canary/utils/serializers/output/emails.js +0 -22
- package/core/server/api/canary/utils/serializers/output/identities.js +0 -7
- package/core/server/api/canary/utils/serializers/output/member-signin-urls.js +0 -7
- package/core/server/api/canary/utils/serializers/output/snippets.js +0 -107
- 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
|
+
});
|
package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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} -
|
|
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} -
|
|
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} -
|
|
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} -
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|
|
@@ -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.
|
|
202
|
-
font-weight:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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: #
|
|
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: #
|
|
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:
|
|
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:
|
|
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
|
+
*/
|
|
@@ -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
|
|
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.
|
|
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-
|
|
41
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
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-
|
|
60
|
-
<script src="assets/ghost.min-
|
|
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.
|
|
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-
|
|
41
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
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-
|
|
60
|
-
<script src="assets/ghost.min-
|
|
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.
|
|
132
|
-
"version": "1.
|
|
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
|
+
}
|