ghost 5.9.4 → 5.11.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/components/tryghost-adapter-manager-5.11.0.tgz +0 -0
- package/components/tryghost-api-framework-5.11.0.tgz +0 -0
- package/components/{tryghost-api-version-compatibility-service-0.0.0.tgz → tryghost-api-version-compatibility-service-5.11.0.tgz} +0 -0
- package/components/tryghost-bootstrap-socket-5.11.0.tgz +0 -0
- package/components/tryghost-constants-5.11.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.11.0.tgz +0 -0
- package/components/tryghost-domain-events-5.11.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.11.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.11.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.11.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.11.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.11.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.11.0.tgz +0 -0
- package/components/tryghost-job-manager-5.11.0.tgz +0 -0
- package/components/tryghost-magic-link-5.11.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.11.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.11.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.11.0.tgz +0 -0
- package/components/tryghost-member-events-5.11.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.11.0.tgz +0 -0
- package/components/tryghost-members-api-5.11.0.tgz +0 -0
- package/components/{tryghost-members-csv-0.0.0.tgz → tryghost-members-csv-5.11.0.tgz} +0 -0
- package/components/tryghost-members-events-service-5.11.0.tgz +0 -0
- package/components/tryghost-members-importer-5.11.0.tgz +0 -0
- package/components/tryghost-members-offers-5.11.0.tgz +0 -0
- package/components/tryghost-members-payments-5.11.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.11.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.11.0.tgz +0 -0
- package/components/tryghost-minifier-5.11.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.11.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.11.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.11.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.11.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.11.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.11.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.11.0.tgz +0 -0
- package/components/tryghost-package-json-5.11.0.tgz +0 -0
- package/components/tryghost-security-5.11.0.tgz +0 -0
- package/components/tryghost-session-service-5.11.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.11.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.11.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.11.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.11.0.tgz +0 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +8 -5
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +2 -0
- package/core/bridge.js +2 -0
- package/core/built/admin/assets/chunk.143.14589cc066b8120b73e3.js +49 -0
- package/core/built/admin/assets/{chunk.174.eec7f6398cef4c3e2485.js → chunk.174.ae492405065373dbe102.js} +31 -29
- package/core/built/admin/assets/{chunk.178.506264293194a4922091.js → chunk.178.131e85a10d2031148425.js} +4 -4
- package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js → chunk.579.65e09dd89eec70d059a0.js} +23 -28
- package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js.LICENSE.txt → chunk.579.65e09dd89eec70d059a0.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/ghost-1b0d7c731511bb738ec457d2932c43c0.css +1 -0
- package/core/built/admin/assets/{ghost-b441c9cfa2e31453e86460e50ae7e378.js → ghost-40f5bd12d121c54bbc39e7939e78244f.js} +827 -611
- package/core/built/admin/assets/ghost-dark-7b2825a050b0382630180f48aa78ea5d.css +1 -0
- package/core/built/admin/assets/icons/calendar-stroke.svg +1 -0
- package/core/built/admin/assets/icons/ghost-orb-pink.svg +10 -0
- package/core/built/admin/assets/icons/pen-stroke.svg +1 -0
- package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
- package/core/built/admin/assets/{vendor-516c9e43b4aeb92079dc1ab92c9ce492.js → vendor-741dc0e4078e044a0c9bfaad104de8b3.js} +85 -78
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/ghost_head.js +4 -0
- package/core/frontend/helpers/search.js +42 -0
- package/core/frontend/services/member-attribution-assets/index.js +4 -0
- package/core/frontend/services/member-attribution-assets/service.js +83 -0
- package/core/frontend/src/member-attribution/.eslintrc +10 -0
- package/core/frontend/src/member-attribution/member-attribution.js +90 -0
- package/core/frontend/web/site.js +3 -0
- package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +7 -0
- package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +7 -0
- package/core/server/api/endpoints/comments-members.js +10 -7
- package/core/server/api/endpoints/invites.js +1 -9
- package/core/server/api/endpoints/labels.js +1 -7
- package/core/server/api/endpoints/members.js +3 -13
- package/core/server/api/endpoints/offers.js +2 -2
- package/core/server/api/endpoints/pages.js +2 -10
- package/core/server/api/endpoints/posts.js +11 -10
- package/core/server/api/endpoints/snippets.js +1 -9
- package/core/server/api/endpoints/tags.js +1 -7
- package/core/server/api/endpoints/utils/serializers/input/pages.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -0
- package/core/server/api/endpoints/utils/serializers/output/members.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/site.js +1 -0
- package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +6 -7
- package/core/server/api/endpoints/webhooks.js +2 -19
- package/core/server/data/exporter/table-lists.js +2 -0
- package/core/server/data/migrations/versions/5.10/2022-08-15-05-34-add-expiry-at-column-to-members-products.js +6 -0
- package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-member-created-events-table.js +11 -0
- package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-subscription-created-events-table.js +11 -0
- package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js +45 -0
- package/core/server/data/migrations/versions/5.11/2022-08-22-11-03-add-member-alert-settings-columns-to-users.js +21 -0
- package/core/server/data/migrations/versions/5.11/2022-08-23-13-41-backfill-members-created-events.js +32 -0
- package/core/server/data/migrations/versions/5.11/2022-08-23-13-59-fix-page-resource-type.js +22 -0
- package/core/server/data/schema/fixtures/fixtures.json +3 -0
- package/core/server/data/schema/schema.js +24 -2
- package/core/server/lib/image/cached-image-size-from-url.js +52 -28
- package/core/server/lib/image/gravatar.js +8 -7
- package/core/server/lib/image/image-size.js +60 -56
- package/core/server/lib/image/image-utils.js +5 -2
- package/core/server/lib/image/index.js +14 -1
- package/core/server/models/action.js +0 -10
- package/core/server/models/api-key.js +3 -18
- package/core/server/models/base/plugins/actions.js +55 -0
- package/core/server/models/integration.js +3 -0
- package/core/server/models/label.js +3 -18
- package/core/server/models/member-created-event.js +26 -0
- package/core/server/models/member.js +54 -4
- package/core/server/models/offer.js +3 -0
- package/core/server/models/post.js +25 -18
- package/core/server/models/product.js +3 -0
- package/core/server/models/settings.js +4 -0
- package/core/server/models/subscription-created-event.js +30 -0
- package/core/server/models/tag.js +3 -18
- package/core/server/models/user.js +7 -19
- package/core/server/models/webhook.js +3 -0
- package/core/server/services/auth/api-key/admin.js +0 -3
- package/core/server/services/auth/passwordreset.js +0 -3
- package/core/server/services/comments/emails.js +3 -3
- package/core/server/services/explore/service.js +8 -6
- package/core/server/services/member-attribution/index.js +52 -0
- package/core/server/services/members/api.js +3 -1
- package/core/server/services/members/jobs/clean-expired-comped.js +105 -0
- package/core/server/services/members/jobs/index.js +27 -0
- package/core/server/services/members/service.js +14 -8
- package/core/server/services/public-config/site.js +1 -0
- package/core/server/services/route-settings/default-settings-manager.js +19 -17
- package/core/server/services/settings/settings-service.js +1 -1
- package/core/server/services/webhooks/trigger.js +14 -5
- package/core/shared/config/defaults.json +8 -3
- package/core/shared/labs.js +5 -2
- package/package.json +84 -83
- package/yarn.lock +440 -615
- package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
- package/components/tryghost-api-framework-0.0.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
- package/components/tryghost-constants-0.0.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
- package/components/tryghost-domain-events-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
- package/components/tryghost-extract-api-key-0.0.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-0.0.0.tgz +0 -0
- package/components/tryghost-job-manager-0.0.0.tgz +0 -0
- package/components/tryghost-magic-link-0.0.0.tgz +0 -0
- package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-member-events-0.0.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
- package/components/tryghost-members-api-0.0.0.tgz +0 -0
- package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
- package/components/tryghost-members-importer-0.0.0.tgz +0 -0
- package/components/tryghost-members-offers-0.0.0.tgz +0 -0
- package/components/tryghost-members-payments-0.0.0.tgz +0 -0
- package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
- package/components/tryghost-minifier-0.0.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-0.0.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-0.0.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-0.0.0.tgz +0 -0
- package/components/tryghost-mw-vhost-0.0.0.tgz +0 -0
- package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
- package/components/tryghost-package-json-0.0.0.tgz +0 -0
- package/components/tryghost-security-0.0.0.tgz +0 -0
- package/components/tryghost-session-service-0.0.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
- package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
- package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
- package/core/built/admin/assets/chunk.143.1c158e8ef19f10e5439c.js +0 -41
- package/core/built/admin/assets/ghost-dark-4080c8f100997d4b8947f5da0e7946a1.css +0 -1
- package/core/built/admin/assets/ghost-facfdf4a7d9759c5b681340805f21fd8.css +0 -1
|
@@ -13,6 +13,9 @@ Label = ghostBookshelf.Model.extend({
|
|
|
13
13
|
|
|
14
14
|
tableName: 'labels',
|
|
15
15
|
|
|
16
|
+
actionsCollectCRUD: true,
|
|
17
|
+
actionsResourceType: 'label',
|
|
18
|
+
|
|
16
19
|
emitChange: function emitChange(event, options) {
|
|
17
20
|
const eventToTrigger = 'label' + '.' + event;
|
|
18
21
|
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
|
@@ -62,24 +65,6 @@ Label = ghostBookshelf.Model.extend({
|
|
|
62
65
|
const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
|
63
66
|
|
|
64
67
|
return attrs;
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
getAction(event, options) {
|
|
68
|
-
const actor = this.getActor(options);
|
|
69
|
-
|
|
70
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
71
|
-
if (!actor) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// @TODO: implement context
|
|
76
|
-
return {
|
|
77
|
-
event: event,
|
|
78
|
-
resource_id: this.id || this.previous('id'),
|
|
79
|
-
resource_type: 'label',
|
|
80
|
-
actor_id: actor.id,
|
|
81
|
-
actor_type: actor.type
|
|
82
|
-
};
|
|
83
68
|
}
|
|
84
69
|
}, {
|
|
85
70
|
orderDefaultOptions: function orderDefaultOptions() {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const ghostBookshelf = require('./base');
|
|
3
|
+
|
|
4
|
+
const MemberCreatedEvent = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'members_created_events',
|
|
6
|
+
|
|
7
|
+
member() {
|
|
8
|
+
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
attribution() {
|
|
12
|
+
return this.belongsTo('Post', 'attribution_id', 'id');
|
|
13
|
+
}
|
|
14
|
+
}, {
|
|
15
|
+
async edit() {
|
|
16
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberCreatedEvent'});
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async destroy() {
|
|
20
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberCreatedEvent'});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
MemberCreatedEvent: ghostBookshelf.model('MemberCreatedEvent', MemberCreatedEvent)
|
|
26
|
+
};
|
|
@@ -3,6 +3,7 @@ const uuid = require('uuid');
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const config = require('../../shared/config');
|
|
5
5
|
const {gravatar} = require('../lib/image');
|
|
6
|
+
const labs = require('../../shared/labs');
|
|
6
7
|
|
|
7
8
|
const Member = ghostBookshelf.Model.extend({
|
|
8
9
|
tableName: 'members',
|
|
@@ -39,6 +40,12 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
39
40
|
}, {
|
|
40
41
|
key: 'newsletters',
|
|
41
42
|
replacement: 'newsletters.slug'
|
|
43
|
+
}, {
|
|
44
|
+
key: 'signup',
|
|
45
|
+
replacement: 'signups.attribution_id'
|
|
46
|
+
}, {
|
|
47
|
+
key: 'conversion',
|
|
48
|
+
replacement: 'conversions.attribution_id'
|
|
42
49
|
}];
|
|
43
50
|
},
|
|
44
51
|
|
|
@@ -73,6 +80,18 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
73
80
|
joinFrom: 'member_id',
|
|
74
81
|
joinTo: 'customer_id',
|
|
75
82
|
joinToForeign: 'customer_id'
|
|
83
|
+
},
|
|
84
|
+
signups: {
|
|
85
|
+
tableName: 'members_created_events',
|
|
86
|
+
tableNameAs: 'signups',
|
|
87
|
+
type: 'oneToOne',
|
|
88
|
+
joinFrom: 'member_id'
|
|
89
|
+
},
|
|
90
|
+
conversions: {
|
|
91
|
+
tableName: 'members_subscription_created_events',
|
|
92
|
+
tableNameAs: 'conversions',
|
|
93
|
+
type: 'oneToOne',
|
|
94
|
+
joinFrom: 'member_id'
|
|
76
95
|
}
|
|
77
96
|
};
|
|
78
97
|
},
|
|
@@ -102,12 +121,16 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
102
121
|
|
|
103
122
|
products() {
|
|
104
123
|
return this.belongsToMany('Product', 'members_products', 'member_id', 'product_id')
|
|
105
|
-
.withPivot('sort_order')
|
|
124
|
+
.withPivot('sort_order', 'expiry_at')
|
|
106
125
|
.query('orderBy', 'sort_order', 'ASC')
|
|
107
126
|
.query((qb) => {
|
|
108
127
|
// avoids bookshelf adding a `DISTINCT` to the query
|
|
109
128
|
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
110
|
-
|
|
129
|
+
if (labs.isSet('compExpiring')) {
|
|
130
|
+
qb.columns('products.*', 'expiry_at');
|
|
131
|
+
} else {
|
|
132
|
+
qb.columns('products.*');
|
|
133
|
+
}
|
|
111
134
|
});
|
|
112
135
|
},
|
|
113
136
|
|
|
@@ -155,6 +178,21 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
155
178
|
return this.hasMany('EmailRecipient', 'member_id', 'id');
|
|
156
179
|
},
|
|
157
180
|
|
|
181
|
+
async updateTierExpiry(products = [], options = {}) {
|
|
182
|
+
if (!labs.isSet('compExpiring')) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
for (const product of products) {
|
|
186
|
+
if (product?.expiry_at) {
|
|
187
|
+
const expiry = new Date(product.expiry_at);
|
|
188
|
+
const queryOptions = _.extend({}, options, {
|
|
189
|
+
query: {where: {product_id: product.id}}
|
|
190
|
+
});
|
|
191
|
+
await this.products().updatePivot({expiry_at: expiry}, queryOptions);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
158
196
|
serialize(options) {
|
|
159
197
|
const defaultSerializedObject = ghostBookshelf.Model.prototype.serialize.call(this, options);
|
|
160
198
|
|
|
@@ -344,7 +382,13 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
344
382
|
return this.add(data, Object.assign({transacting}, unfilteredOptions));
|
|
345
383
|
});
|
|
346
384
|
}
|
|
347
|
-
|
|
385
|
+
|
|
386
|
+
return ghostBookshelf.Model.add.call(this, data, unfilteredOptions).then(async (member) => {
|
|
387
|
+
if (data.products) {
|
|
388
|
+
await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
|
|
389
|
+
}
|
|
390
|
+
return member;
|
|
391
|
+
});
|
|
348
392
|
},
|
|
349
393
|
|
|
350
394
|
edit(data, unfilteredOptions = {}) {
|
|
@@ -353,7 +397,13 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
353
397
|
return this.edit(data, Object.assign({transacting}, unfilteredOptions));
|
|
354
398
|
});
|
|
355
399
|
}
|
|
356
|
-
|
|
400
|
+
|
|
401
|
+
return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions).then(async (member) => {
|
|
402
|
+
if (data.products) {
|
|
403
|
+
await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
|
|
404
|
+
}
|
|
405
|
+
return member;
|
|
406
|
+
});
|
|
357
407
|
},
|
|
358
408
|
|
|
359
409
|
destroy(unfilteredOptions = {}) {
|
|
@@ -38,6 +38,10 @@ Post = ghostBookshelf.Model.extend({
|
|
|
38
38
|
|
|
39
39
|
tableName: 'posts',
|
|
40
40
|
|
|
41
|
+
actionsCollectCRUD: true,
|
|
42
|
+
actionsResourceType: 'post',
|
|
43
|
+
actionsExtraContext: ['type'],
|
|
44
|
+
|
|
41
45
|
/**
|
|
42
46
|
* @NOTE
|
|
43
47
|
*
|
|
@@ -962,24 +966,6 @@ Post = ghostBookshelf.Model.extend({
|
|
|
962
966
|
|
|
963
967
|
delete options.status;
|
|
964
968
|
return filter;
|
|
965
|
-
},
|
|
966
|
-
|
|
967
|
-
getAction(event, options) {
|
|
968
|
-
const actor = this.getActor(options);
|
|
969
|
-
|
|
970
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
971
|
-
if (!actor) {
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// @TODO: implement context
|
|
976
|
-
return {
|
|
977
|
-
event: event,
|
|
978
|
-
resource_id: this.id || this.previous('id'),
|
|
979
|
-
resource_type: 'post',
|
|
980
|
-
actor_id: actor.id,
|
|
981
|
-
actor_type: actor.type
|
|
982
|
-
};
|
|
983
969
|
}
|
|
984
970
|
}, {
|
|
985
971
|
allowedFormats: ['mobiledoc', 'html', 'plaintext'],
|
|
@@ -1262,6 +1248,27 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1262
1248
|
return Promise.reject(new errors.NoPermissionError({
|
|
1263
1249
|
message: tpl(messages.notEnoughPermission)
|
|
1264
1250
|
}));
|
|
1251
|
+
},
|
|
1252
|
+
|
|
1253
|
+
countRelations() {
|
|
1254
|
+
return {
|
|
1255
|
+
signups(modelOrCollection) {
|
|
1256
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1257
|
+
qb.count('members_created_events.id')
|
|
1258
|
+
.from('members_created_events')
|
|
1259
|
+
.whereRaw('posts.id = members_created_events.attribution_id')
|
|
1260
|
+
.as('count__signups');
|
|
1261
|
+
});
|
|
1262
|
+
},
|
|
1263
|
+
conversions(modelOrCollection) {
|
|
1264
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1265
|
+
qb.count('members_subscription_created_events.id')
|
|
1266
|
+
.from('members_subscription_created_events')
|
|
1267
|
+
.whereRaw('posts.id = members_subscription_created_events.attribution_id')
|
|
1268
|
+
.as('count__conversions');
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1265
1272
|
}
|
|
1266
1273
|
});
|
|
1267
1274
|
|
|
@@ -97,6 +97,10 @@ Settings = ghostBookshelf.Model.extend({
|
|
|
97
97
|
|
|
98
98
|
tableName: 'settings',
|
|
99
99
|
|
|
100
|
+
actionsCollectCRUD: true,
|
|
101
|
+
actionsResourceType: 'setting',
|
|
102
|
+
actionsExtraContext: ['key', 'group'],
|
|
103
|
+
|
|
100
104
|
emitChange: function emitChange(event, options) {
|
|
101
105
|
const eventToTrigger = 'settings' + '.' + event;
|
|
102
106
|
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const ghostBookshelf = require('./base');
|
|
3
|
+
|
|
4
|
+
const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'members_subscription_created_events',
|
|
6
|
+
|
|
7
|
+
member() {
|
|
8
|
+
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
subscription() {
|
|
12
|
+
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
attribution() {
|
|
16
|
+
return this.belongsTo('Post', 'attribution_id', 'id');
|
|
17
|
+
}
|
|
18
|
+
}, {
|
|
19
|
+
async edit() {
|
|
20
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit SubscriptionCreatedEvent'});
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async destroy() {
|
|
24
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy SubscriptionCreatedEvent'});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
SubscriptionCreatedEvent: ghostBookshelf.model('SubscriptionCreatedEvent', SubscriptionCreatedEvent)
|
|
30
|
+
};
|
|
@@ -14,6 +14,9 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
14
14
|
|
|
15
15
|
tableName: 'tags',
|
|
16
16
|
|
|
17
|
+
actionsCollectCRUD: true,
|
|
18
|
+
actionsResourceType: 'tag',
|
|
19
|
+
|
|
17
20
|
defaults: function defaults() {
|
|
18
21
|
return {
|
|
19
22
|
visibility: 'public'
|
|
@@ -138,24 +141,6 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
138
141
|
return attrs;
|
|
139
142
|
},
|
|
140
143
|
|
|
141
|
-
getAction(event, options) {
|
|
142
|
-
const actor = this.getActor(options);
|
|
143
|
-
|
|
144
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
145
|
-
if (!actor) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// @TODO: implement context
|
|
150
|
-
return {
|
|
151
|
-
event: event,
|
|
152
|
-
resource_id: this.id || this.previous('id'),
|
|
153
|
-
resource_type: 'tag',
|
|
154
|
-
actor_id: actor.id,
|
|
155
|
-
actor_type: actor.type
|
|
156
|
-
};
|
|
157
|
-
},
|
|
158
|
-
|
|
159
144
|
defaultColumnsToFetch() {
|
|
160
145
|
return ['id'];
|
|
161
146
|
}
|
|
@@ -57,12 +57,18 @@ User = ghostBookshelf.Model.extend({
|
|
|
57
57
|
|
|
58
58
|
tableName: 'users',
|
|
59
59
|
|
|
60
|
+
actionsCollectCRUD: true,
|
|
61
|
+
actionsResourceType: 'user',
|
|
62
|
+
|
|
60
63
|
defaults: function defaults() {
|
|
61
64
|
return {
|
|
62
65
|
password: security.identifier.uid(50),
|
|
63
66
|
visibility: 'public',
|
|
64
67
|
status: 'active',
|
|
65
|
-
comment_notifications: true
|
|
68
|
+
comment_notifications: true,
|
|
69
|
+
free_member_signup_notification: true,
|
|
70
|
+
paid_subscription_started_notification: true,
|
|
71
|
+
paid_subscription_canceled_notification: false
|
|
66
72
|
};
|
|
67
73
|
},
|
|
68
74
|
|
|
@@ -363,24 +369,6 @@ User = ghostBookshelf.Model.extend({
|
|
|
363
369
|
delete options.status;
|
|
364
370
|
|
|
365
371
|
return filter;
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
getAction(event, options) {
|
|
369
|
-
const actor = this.getActor(options);
|
|
370
|
-
|
|
371
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
372
|
-
if (!actor) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// @TODO: implement context
|
|
377
|
-
return {
|
|
378
|
-
event: event,
|
|
379
|
-
resource_id: this.id || this.previous('id'),
|
|
380
|
-
resource_type: 'user',
|
|
381
|
-
actor_id: actor.id,
|
|
382
|
-
actor_type: actor.type
|
|
383
|
-
};
|
|
384
372
|
}
|
|
385
373
|
}, {
|
|
386
374
|
orderDefaultOptions: function orderDefaultOptions() {
|
|
@@ -144,9 +144,6 @@ function doReset(options, tokenParts, settingsAPI) {
|
|
|
144
144
|
updatedUser.set('status', 'active');
|
|
145
145
|
return updatedUser.save(options);
|
|
146
146
|
})
|
|
147
|
-
.catch(errors.ValidationError, (err) => {
|
|
148
|
-
return Promise.reject(err);
|
|
149
|
-
})
|
|
150
147
|
.catch((err) => {
|
|
151
148
|
if (errors.utils.isGhostError(err)) {
|
|
152
149
|
return Promise.reject(err);
|
|
@@ -45,7 +45,7 @@ class CommentsServiceEmails {
|
|
|
45
45
|
accentColor: this.settingsCache.get('accent_color'),
|
|
46
46
|
fromEmail: this.notificationFromAddress,
|
|
47
47
|
toEmail: to,
|
|
48
|
-
staffUrl:
|
|
48
|
+
staffUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${author.get('slug')}`)
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
const {html, text} = await this.renderEmailTemplate('new-comment', templateData);
|
|
@@ -132,7 +132,7 @@ class CommentsServiceEmails {
|
|
|
132
132
|
commentHtml: comment.get('html'),
|
|
133
133
|
commentText: htmlToPlaintext.comment(comment.get('html')),
|
|
134
134
|
commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
reporterName: reporter.name,
|
|
137
137
|
reporterEmail: reporter.email,
|
|
138
138
|
reporter: reporter.name ? `${reporter.name} (${reporter.email})` : reporter.email,
|
|
@@ -144,7 +144,7 @@ class CommentsServiceEmails {
|
|
|
144
144
|
accentColor: this.settingsCache.get('accent_color'),
|
|
145
145
|
fromEmail: this.notificationFromAddress,
|
|
146
146
|
toEmail: to,
|
|
147
|
-
staffUrl:
|
|
147
|
+
staffUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${owner.get('slug')}`)
|
|
148
148
|
};
|
|
149
149
|
|
|
150
150
|
const {html, text} = await this.renderEmailTemplate('report', templateData);
|
|
@@ -26,17 +26,19 @@ module.exports = class ExploreService {
|
|
|
26
26
|
const totalMembers = await this.MembersService.stats.getTotalMembers();
|
|
27
27
|
const mrrStats = await this.StatsService.getMRRHistory();
|
|
28
28
|
|
|
29
|
-
const {description, icon, title, url} = this.PublicConfigService.site;
|
|
29
|
+
const {description, icon, title, url, accent_color: accentColor, locale} = this.PublicConfigService.site;
|
|
30
30
|
|
|
31
31
|
const exploreProperties = {
|
|
32
32
|
version: ghostVersion.full,
|
|
33
|
-
totalMembers,
|
|
34
|
-
mrrStats,
|
|
33
|
+
total_members: totalMembers,
|
|
34
|
+
mrr_stats: mrrStats,
|
|
35
35
|
site: {
|
|
36
36
|
description,
|
|
37
37
|
icon,
|
|
38
38
|
title,
|
|
39
|
-
url
|
|
39
|
+
url,
|
|
40
|
+
accent_color: accentColor,
|
|
41
|
+
locale
|
|
40
42
|
},
|
|
41
43
|
stripe: {
|
|
42
44
|
configured: this.StripeService.api.configured,
|
|
@@ -45,10 +47,10 @@ module.exports = class ExploreService {
|
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
const mostRecentlyPublishedPost = await this.PostsService.getMostRecentlyPublishedPost();
|
|
48
|
-
exploreProperties.
|
|
50
|
+
exploreProperties.most_recently_published_at = mostRecentlyPublishedPost?.get('published_at') || null;
|
|
49
51
|
|
|
50
52
|
const owner = await this.UserModel.findOne({role: 'Owner', status: 'all'});
|
|
51
|
-
exploreProperties.
|
|
53
|
+
exploreProperties.owner_email = owner?.get('email') || null;
|
|
52
54
|
|
|
53
55
|
return exploreProperties;
|
|
54
56
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const urlService = require('../url');
|
|
2
|
+
const labsService = require('../../../shared/labs');
|
|
3
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
4
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
5
|
+
|
|
6
|
+
class MemberAttributionServiceWrapper {
|
|
7
|
+
init() {
|
|
8
|
+
if (this.eventHandler) {
|
|
9
|
+
// Prevent creating duplicate DomainEvents subscribers
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Wire up all the dependencies
|
|
14
|
+
const {MemberAttributionService, UrlTranslator, AttributionBuilder, EventHandler} = require('@tryghost/member-attribution');
|
|
15
|
+
const models = require('../../models');
|
|
16
|
+
|
|
17
|
+
const urlTranslator = new UrlTranslator({
|
|
18
|
+
urlService,
|
|
19
|
+
urlUtils,
|
|
20
|
+
models: {
|
|
21
|
+
Post: models.Post,
|
|
22
|
+
User: models.User,
|
|
23
|
+
Tag: models.Tag
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const attributionBuilder = new AttributionBuilder({urlTranslator});
|
|
28
|
+
|
|
29
|
+
// Expose the service
|
|
30
|
+
this.service = new MemberAttributionService({
|
|
31
|
+
models: {
|
|
32
|
+
MemberCreatedEvent: models.MemberCreatedEvent,
|
|
33
|
+
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
|
|
34
|
+
},
|
|
35
|
+
attributionBuilder,
|
|
36
|
+
labsService
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Listen for events and store them in the database
|
|
40
|
+
this.eventHandler = new EventHandler({
|
|
41
|
+
models: {
|
|
42
|
+
MemberCreatedEvent: models.MemberCreatedEvent,
|
|
43
|
+
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
|
|
44
|
+
},
|
|
45
|
+
DomainEvents,
|
|
46
|
+
labsService
|
|
47
|
+
});
|
|
48
|
+
this.eventHandler.subscribe();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = new MemberAttributionServiceWrapper();
|
|
@@ -14,6 +14,7 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
14
14
|
const labsService = require('../../../shared/labs');
|
|
15
15
|
const offersService = require('../offers');
|
|
16
16
|
const newslettersService = require('../newsletters');
|
|
17
|
+
const memberAttributionService = require('../member-attribution');
|
|
17
18
|
|
|
18
19
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
19
20
|
|
|
@@ -195,7 +196,8 @@ function createApiInstance(config) {
|
|
|
195
196
|
stripeAPIService: stripeService.api,
|
|
196
197
|
offersAPI: offersService.api,
|
|
197
198
|
labsService: labsService,
|
|
198
|
-
newslettersService: newslettersService
|
|
199
|
+
newslettersService: newslettersService,
|
|
200
|
+
memberAttributionService: memberAttributionService.service
|
|
199
201
|
});
|
|
200
202
|
|
|
201
203
|
return membersApiInstance;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const {parentPort} = require('worker_threads');
|
|
2
|
+
const ObjectId = require('bson-objectid').default;
|
|
3
|
+
const {chunk: chunkArray} = require('lodash');
|
|
4
|
+
const debug = require('@tryghost/debug')('jobs:clean-expired-comped');
|
|
5
|
+
const moment = require('moment');
|
|
6
|
+
|
|
7
|
+
// recurring job to clean expired complimentary subscriptions
|
|
8
|
+
|
|
9
|
+
// Exit early when cancelled to prevent stalling shutdown. No cleanup needed when cancelling as everything is idempotent and will pick up
|
|
10
|
+
// where it left off on next run
|
|
11
|
+
function cancel() {
|
|
12
|
+
if (parentPort) {
|
|
13
|
+
parentPort.postMessage('Expired complimentary subscriptions cleanup cancelled before completion');
|
|
14
|
+
parentPort.postMessage('cancelled');
|
|
15
|
+
} else {
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}, 1000);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (parentPort) {
|
|
23
|
+
parentPort.once('message', (message) => {
|
|
24
|
+
if (message === 'cancel') {
|
|
25
|
+
return cancel();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
(async () => {
|
|
31
|
+
const cleanupStartDate = new Date();
|
|
32
|
+
const db = require('../../../data/db');
|
|
33
|
+
debug(`Starting cleanup of expired comp subscriptions`);
|
|
34
|
+
const expiredCompedRows = await db.knex('members_products')
|
|
35
|
+
.where('expiry_at', '<', moment.utc().startOf('day').toISOString())
|
|
36
|
+
.select('*');
|
|
37
|
+
|
|
38
|
+
let deletedExpiredSubs = 0;
|
|
39
|
+
let updatedMembers = 0;
|
|
40
|
+
|
|
41
|
+
// Run cleanup for expired comp subscriptions
|
|
42
|
+
// Removes expired comped entries from members_products table
|
|
43
|
+
// Updates affected members status to free from comped
|
|
44
|
+
// Adds member status event for going from comped to free
|
|
45
|
+
if (expiredCompedRows?.length) {
|
|
46
|
+
const rowIds = expiredCompedRows.map(d => d.id);
|
|
47
|
+
const memberIds = expiredCompedRows.map(d => d.member_id);
|
|
48
|
+
|
|
49
|
+
// Delete all expired comped rows
|
|
50
|
+
deletedExpiredSubs = await db.knex('members_products')
|
|
51
|
+
.whereIn('id', rowIds)
|
|
52
|
+
.del();
|
|
53
|
+
|
|
54
|
+
// fetch all comped members to update
|
|
55
|
+
const membersToUpdate = await db.knex('members')
|
|
56
|
+
.whereIn('id', memberIds)
|
|
57
|
+
.andWhere('status', 'comped');
|
|
58
|
+
|
|
59
|
+
const updateMemberIds = membersToUpdate.map(d => d.id);
|
|
60
|
+
|
|
61
|
+
// Update all comped members to free
|
|
62
|
+
updatedMembers = await db.knex('members')
|
|
63
|
+
.whereIn('id', updateMemberIds)
|
|
64
|
+
.update({
|
|
65
|
+
status: 'free'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const statusEvents = membersToUpdate.map((member) => {
|
|
69
|
+
const now = db.knex.raw('CURRENT_TIMESTAMP');
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: ObjectId().toHexString(),
|
|
73
|
+
member_id: member.id,
|
|
74
|
+
from_status: member.status,
|
|
75
|
+
to_status: 'free',
|
|
76
|
+
created_at: now
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// SQLite >= 3.32.0 can support 32766 host parameters
|
|
81
|
+
// each row uses 5 variables so ⌊32766/5⌋ = 6553
|
|
82
|
+
const chunkSize = 6553;
|
|
83
|
+
|
|
84
|
+
const chunks = chunkArray(statusEvents, chunkSize);
|
|
85
|
+
|
|
86
|
+
// Adds status event for members going comped->free
|
|
87
|
+
for (const chunk of chunks) {
|
|
88
|
+
await db.knex('members_status_events').insert(chunk);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let cleanupEndDate = new Date();
|
|
93
|
+
|
|
94
|
+
debug(`Removed ${deletedExpiredSubs} expired subscriptions, updated ${updatedMembers} members in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
|
|
95
|
+
|
|
96
|
+
if (parentPort) {
|
|
97
|
+
parentPort.postMessage(`Removed ${deletedExpiredSubs} expired subscriptions, updated ${updatedMembers} members in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
|
|
98
|
+
parentPort.postMessage('done');
|
|
99
|
+
} else {
|
|
100
|
+
// give the logging pipes time finish writing before exit
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}, 1000);
|
|
104
|
+
}
|
|
105
|
+
})();
|