ghost 4.32.0 → 4.33.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/LICENSE +1 -1
- package/README.md +1 -1
- package/core/boot.js +3 -0
- package/core/built/assets/{chunk.3.8f95b516d88ff4eec64c.js → chunk.3.4906cf0b01d6d8e33374.js} +134 -130
- package/core/built/assets/{ghost-dark-43f5faa616791819b3ae91e128ec41f0.css → ghost-dark-661a50922267648a0362c3d367a22013.css} +1 -1
- package/core/built/assets/{ghost.min-c3f7cbabcc1a69476534453c6c747ee3.css → ghost.min-1f0218f33e08f8d69b2159977d0c9318.css} +1 -1
- package/core/built/assets/{ghost.min-2b20489c79323b165909749382adc158.js → ghost.min-501554f903f29164473a5dc620caaddb.js} +719 -726
- package/core/built/assets/img/apple-touch-icon-74680e326a7e87b159d366c7d4fb3d4b.png +0 -0
- package/core/built/assets/img/large-ac90af7c93a4b47e8d956fa9fef31d9d.png +0 -0
- package/core/built/assets/img/medium-fef07013cffd5c45a655a250912a0ad7.png +0 -0
- package/core/built/assets/img/small-b90396925485f17b2ca82c31be42de5f.png +0 -0
- package/core/built/assets/img/touch-icon-ipad-2e78629d62ad05746f980f14623dfadb.png +0 -0
- package/core/built/assets/img/touch-icon-iphone-93ed4382d391be9180093fd77ce8f410.png +0 -0
- package/core/built/assets/{vendor.min-987af30228885bce50f05c4723fe6f53.css → vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css} +1 -1
- package/core/built/assets/{vendor.min-992a9b07f7d0a67b5a4afd91319edf8b.js → vendor.min-d43620e98444a46441495445f4c155f8.js} +1407 -1455
- package/core/frontend/apps/amp/lib/views/amp.hbs +4 -4
- package/core/frontend/helpers/date.js +3 -4
- package/core/frontend/meta/description.js +3 -3
- package/core/frontend/services/routing/config/canary.js +1 -1
- package/core/frontend/services/routing/config/v4.js +1 -1
- package/core/frontend/services/sitemap/base-generator.js +21 -18
- package/core/frontend/services/sitemap/handler.js +13 -4
- package/core/frontend/services/sitemap/index-generator.js +20 -10
- package/core/frontend/services/sitemap/manager.js +8 -5
- package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +3 -1
- package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +1 -6
- package/core/frontend/src/cards/css/audio.css +5 -0
- package/core/frontend/src/cards/css/bookmark.css +5 -0
- package/core/frontend/src/cards/css/button.css +5 -0
- package/core/frontend/src/cards/css/callout.css +5 -0
- package/core/frontend/src/cards/css/file.css +6 -1
- package/core/frontend/src/cards/css/gallery.css +5 -0
- package/core/frontend/src/cards/css/header.css +5 -0
- package/core/frontend/src/cards/css/nft.css +5 -0
- package/core/frontend/src/cards/css/product.css +5 -0
- package/core/frontend/src/cards/css/toggle.css +5 -0
- package/core/frontend/src/cards/css/video.css +4 -0
- package/core/frontend/views/unsubscribe.hbs +12 -7
- package/core/frontend/web/site.js +7 -4
- package/core/server/api/canary/settings.js +2 -1
- package/core/server/api/canary/utils/serializers/output/products.js +4 -0
- package/core/server/data/db/info.js +4 -0
- package/core/server/data/migrations/versions/4.33/2022-01-14-11-50-add-type-column-to-products.js +12 -0
- package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +37 -0
- package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +46 -0
- package/core/server/data/migrations/versions/4.33/2022-01-19-10-43-add-active-column-to-products-table.js +7 -0
- package/core/server/data/schema/default-settings.json +1 -1
- package/core/server/data/schema/fixtures/fixtures.json +9 -1
- package/core/server/data/schema/schema.js +2 -0
- package/core/server/models/base/plugins/data-manipulation.js +3 -2
- package/core/server/models/product.js +4 -0
- package/core/server/models/single-use-token.js +1 -1
- package/core/server/models/tag.js +8 -0
- package/core/server/services/mega/template.js +4 -2
- package/core/server/services/members/api.js +2 -16
- package/core/server/services/members/config.js +1 -9
- package/core/server/services/members/middleware.js +5 -3
- package/core/server/services/members/service.js +19 -46
- package/core/server/services/offers/service.js +1 -4
- package/core/server/services/public-config/config.js +3 -2
- package/core/server/services/stripe/config.js +24 -9
- package/core/server/services/stripe/index.js +36 -28
- package/core/server/services/themes/activation-bridge.js +3 -10
- package/core/server/services/themes/index.js +0 -21
- package/core/server/services/twitter-embed.js +1 -2
- package/core/server/update-check.js +2 -1
- package/core/server/web/admin/views/default-prod.html +10 -13
- package/core/server/web/admin/views/default.html +10 -13
- package/core/server/web/api/canary/admin/routes.js +2 -6
- package/core/server/web/members/app.js +3 -2
- package/core/server/web/shared/middleware/cache-control.js +12 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +2 -14
- package/package.json +71 -69
- package/yarn.lock +2577 -2997
- package/core/built/assets/img/large-bf46e150380a4979a7389b45f5bb479d.png +0 -0
- package/core/built/assets/img/medium-7359075af28d69523987ff4c0e2067c5.png +0 -0
- package/core/built/assets/img/small-42ff134f320b8b5a6eca3781c4e4b2db.png +0 -0
- package/core/built/assets/img/touch-icon-ipad-3117c0fa950d0fc43c95becef61f4167.png +0 -0
- package/core/built/assets/img/touch-icon-iphone-d2790931c3477664981061ed9fa5242e.png +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
if (knex.client.config.client !== 'mysql') {
|
|
8
|
+
logging.warn('Skipping cleanup of duplicate offer redemptions - database is not MySQL');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
logging.info('Looking for duplicate offer redemptions.');
|
|
12
|
+
|
|
13
|
+
const duplicates = await knex('offer_redemptions')
|
|
14
|
+
.select('subscription_id')
|
|
15
|
+
.count('subscription_id as count')
|
|
16
|
+
.groupBy('subscription_id')
|
|
17
|
+
.having('count', '>', 1);
|
|
18
|
+
|
|
19
|
+
if (!duplicates.length) {
|
|
20
|
+
logging.info('No duplicate offer redemptions found.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
logging.info(`Found ${duplicates.length} duplicate offer redemptions.`);
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
27
|
+
for (const duplicate of duplicates) {
|
|
28
|
+
const offerRedemptions = await knex('offer_redemptions')
|
|
29
|
+
.select('id')
|
|
30
|
+
.where('subscription_id', duplicate.subscription_id);
|
|
31
|
+
|
|
32
|
+
const [offerRedemptionToKeep, ...offerRedemptionsToDelete] = offerRedemptions;
|
|
33
|
+
|
|
34
|
+
logging.info(`Keeping offer redemption ${offerRedemptionToKeep.id}`);
|
|
35
|
+
|
|
36
|
+
logging.info(`Deleting ${offerRedemptionsToDelete.length} duplicates`);
|
|
37
|
+
await knex('offer_redemptions')
|
|
38
|
+
.whereIn('id', offerRedemptionsToDelete.map(x => x.id))
|
|
39
|
+
.del();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async function down() {
|
|
43
|
+
logging.warn('Not recreating duplicate offer redemptions');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
);
|
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
{
|
|
4
4
|
"name": "Product",
|
|
5
5
|
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"name": "Free",
|
|
8
|
+
"slug": "free",
|
|
9
|
+
"type": "free",
|
|
10
|
+
"active": true
|
|
11
|
+
},
|
|
6
12
|
{
|
|
7
13
|
"name": "Default Product",
|
|
8
|
-
"slug": "default-product"
|
|
14
|
+
"slug": "default-product",
|
|
15
|
+
"type": "paid",
|
|
16
|
+
"active": true
|
|
9
17
|
}
|
|
10
18
|
]
|
|
11
19
|
},
|
|
@@ -378,9 +378,11 @@ module.exports = {
|
|
|
378
378
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
379
379
|
name: {type: 'string', maxlength: 191, nullable: false},
|
|
380
380
|
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
381
|
+
active: {type: 'boolean', nullable: false, defaultTo: true},
|
|
381
382
|
monthly_price_id: {type: 'string', maxlength: 24, nullable: true},
|
|
382
383
|
yearly_price_id: {type: 'string', maxlength: 24, nullable: true},
|
|
383
384
|
description: {type: 'string', maxlength: 191, nullable: true},
|
|
385
|
+
type: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'paid', validations: {isIn: [['paid', 'free']]}},
|
|
384
386
|
created_at: {type: 'dateTime', nullable: false},
|
|
385
387
|
updated_at: {type: 'dateTime', nullable: true}
|
|
386
388
|
},
|
|
@@ -75,8 +75,9 @@ module.exports = function (Bookshelf) {
|
|
|
75
75
|
fixBools: function fixBools(attrs) {
|
|
76
76
|
const self = this;
|
|
77
77
|
_.each(attrs, function each(value, key) {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const tableDef = schema.tables[self.tableName];
|
|
79
|
+
const columnDef = tableDef ? tableDef[key] : null;
|
|
80
|
+
if (columnDef && (columnDef.type === 'bool' || columnDef.type === 'boolean')) {
|
|
80
81
|
attrs[key] = value ? true : false;
|
|
81
82
|
}
|
|
82
83
|
});
|
|
@@ -100,6 +100,14 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
100
100
|
|
|
101
101
|
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
|
102
102
|
|
|
103
|
+
// Support tag creation with `posts: [{..., tags: [{slug: 'new'}]}]`
|
|
104
|
+
// In that situation we have a slug but no name so validation will fail
|
|
105
|
+
// unless we set one automatically. Re-using slug for name matches our
|
|
106
|
+
// opposite name->slug behaviour.
|
|
107
|
+
if (!newTag.get('name') && newTag.get('slug')) {
|
|
108
|
+
this.set('name', newTag.get('slug'));
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
// name: #later slug: hash-later
|
|
104
112
|
if (/^#/.test(newTag.get('name'))) {
|
|
105
113
|
this.set('visibility', 'internal');
|
|
@@ -407,6 +407,10 @@ figure blockquote p {
|
|
|
407
407
|
text-decoration: underline;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
a[data-flickr-embed] img {
|
|
411
|
+
height: auto;
|
|
412
|
+
}
|
|
413
|
+
|
|
410
414
|
.kg-bookmark-card {
|
|
411
415
|
width: 100%;
|
|
412
416
|
background: #ffffff;
|
|
@@ -1106,8 +1110,6 @@ ${ templateSettings.showBadge ? `
|
|
|
1106
1110
|
}
|
|
1107
1111
|
` : ''}
|
|
1108
1112
|
|
|
1109
|
-
/* ----- ENDIF THE BROWSER ----- */
|
|
1110
|
-
|
|
1111
1113
|
</style>
|
|
1112
1114
|
</head>
|
|
1113
1115
|
|
|
@@ -25,7 +25,7 @@ function createApiInstance(config) {
|
|
|
25
25
|
tokenConfig: config.getTokenConfig(),
|
|
26
26
|
auth: {
|
|
27
27
|
getSigninURL: config.getSigninURL.bind(config),
|
|
28
|
-
allowSelfSignup: config.getAllowSelfSignup(),
|
|
28
|
+
allowSelfSignup: config.getAllowSelfSignup.bind(config),
|
|
29
29
|
tokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY)
|
|
30
30
|
},
|
|
31
31
|
mail: {
|
|
@@ -173,21 +173,7 @@ function createApiInstance(config) {
|
|
|
173
173
|
stripe: config.getStripePaymentConfig()
|
|
174
174
|
},
|
|
175
175
|
models: {
|
|
176
|
-
|
|
177
|
-
* Settings do not have their own models, so we wrap the webhook in a "fake" model
|
|
178
|
-
*/
|
|
179
|
-
StripeWebhook: {
|
|
180
|
-
async upsert(data, options) {
|
|
181
|
-
const settings = [{
|
|
182
|
-
key: 'members_stripe_webhook_id',
|
|
183
|
-
value: data.webhook_id
|
|
184
|
-
}, {
|
|
185
|
-
key: 'members_stripe_webhook_secret',
|
|
186
|
-
value: data.secret
|
|
187
|
-
}];
|
|
188
|
-
await models.Settings.edit(settings, options);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
176
|
+
EmailRecipient: models.EmailRecipient,
|
|
191
177
|
StripeCustomer: models.MemberStripeCustomer,
|
|
192
178
|
StripeCustomerSubscription: models.StripeCustomerSubscription,
|
|
193
179
|
Member: models.Member,
|
|
@@ -137,8 +137,6 @@ class MembersConfigProvider {
|
|
|
137
137
|
getStripeUrlConfig() {
|
|
138
138
|
const siteUrl = this._urlUtils.getSiteUrl();
|
|
139
139
|
|
|
140
|
-
const webhookHandlerUrl = new URL('members/webhooks/stripe/', siteUrl);
|
|
141
|
-
|
|
142
140
|
const checkoutSuccessUrl = new URL(siteUrl);
|
|
143
141
|
checkoutSuccessUrl.searchParams.set('stripe', 'success');
|
|
144
142
|
const checkoutCancelUrl = new URL(siteUrl);
|
|
@@ -153,8 +151,7 @@ class MembersConfigProvider {
|
|
|
153
151
|
checkoutSuccess: checkoutSuccessUrl.href,
|
|
154
152
|
checkoutCancel: checkoutCancelUrl.href,
|
|
155
153
|
billingSuccess: billingSuccessUrl.href,
|
|
156
|
-
billingCancel: billingCancelUrl.href
|
|
157
|
-
webhookHandler: webhookHandlerUrl.href
|
|
154
|
+
billingCancel: billingCancelUrl.href
|
|
158
155
|
};
|
|
159
156
|
}
|
|
160
157
|
|
|
@@ -175,11 +172,6 @@ class MembersConfigProvider {
|
|
|
175
172
|
checkoutCancelUrl: urls.checkoutCancel,
|
|
176
173
|
billingSuccessUrl: urls.billingSuccess,
|
|
177
174
|
billingCancelUrl: urls.billingCancel,
|
|
178
|
-
webhookHandlerUrl: urls.webhookHandler,
|
|
179
|
-
webhook: {
|
|
180
|
-
id: this._settingsCache.get('members_stripe_webhook_id'),
|
|
181
|
-
secret: this._settingsCache.get('members_stripe_webhook_secret')
|
|
182
|
-
},
|
|
183
175
|
product: {
|
|
184
176
|
name: this._settingsCache.get('stripe_product_name')
|
|
185
177
|
},
|
|
@@ -109,10 +109,13 @@ const getPortalProductPrices = async function () {
|
|
|
109
109
|
monthlyPrice: product.monthlyPrice,
|
|
110
110
|
yearlyPrice: product.yearlyPrice,
|
|
111
111
|
benefits: product.benefits,
|
|
112
|
+
type: product.type,
|
|
112
113
|
prices: productPrices
|
|
113
114
|
};
|
|
114
115
|
});
|
|
115
|
-
const defaultProduct = products
|
|
116
|
+
const defaultProduct = products.find((product) => {
|
|
117
|
+
return product.type === 'paid';
|
|
118
|
+
});
|
|
116
119
|
const defaultPrices = defaultProduct ? defaultProduct.prices : [];
|
|
117
120
|
let portalProducts = defaultProduct ? [defaultProduct] : [];
|
|
118
121
|
if (labsService.isSet('multipleProducts')) {
|
|
@@ -234,6 +237,5 @@ module.exports = {
|
|
|
234
237
|
getOfferData,
|
|
235
238
|
updateMemberData,
|
|
236
239
|
getMemberSiteData,
|
|
237
|
-
deleteSession
|
|
238
|
-
stripeWebhooks: (req, res, next) => membersService.api.middleware.handleStripeWebhook(req, res, next)
|
|
240
|
+
deleteSession
|
|
239
241
|
};
|
|
@@ -5,7 +5,6 @@ const db = require('../../data/db');
|
|
|
5
5
|
const MembersConfigProvider = require('./config');
|
|
6
6
|
const MembersCSVImporter = require('@tryghost/members-importer');
|
|
7
7
|
const MembersStats = require('./stats/members-stats');
|
|
8
|
-
const createMembersApiInstance = require('./api');
|
|
9
8
|
const createMembersSettingsInstance = require('./settings');
|
|
10
9
|
const logging = require('@tryghost/logging');
|
|
11
10
|
const urlUtils = require('../../../shared/url-utils');
|
|
@@ -16,7 +15,6 @@ const models = require('../../models');
|
|
|
16
15
|
const _ = require('lodash');
|
|
17
16
|
const {GhostMailer} = require('../mail');
|
|
18
17
|
const jobsService = require('../jobs');
|
|
19
|
-
const stripeService = require('../stripe');
|
|
20
18
|
|
|
21
19
|
const messages = {
|
|
22
20
|
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
|
|
@@ -26,9 +24,6 @@ const messages = {
|
|
|
26
24
|
emailVerificationEmailMessage: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`
|
|
27
25
|
};
|
|
28
26
|
|
|
29
|
-
// Bind to settings.edited to update systems based on settings changes, similar to the bridge and models/base/listeners
|
|
30
|
-
const events = require('../../lib/common/events');
|
|
31
|
-
|
|
32
27
|
const ghostMailer = new GhostMailer();
|
|
33
28
|
|
|
34
29
|
const membersConfig = new MembersConfigProvider({
|
|
@@ -40,16 +35,6 @@ const membersConfig = new MembersConfigProvider({
|
|
|
40
35
|
let membersApi;
|
|
41
36
|
let membersSettings;
|
|
42
37
|
|
|
43
|
-
function reconfigureMembersAPI() {
|
|
44
|
-
const reconfiguredMembersAPI = createMembersApiInstance(membersConfig);
|
|
45
|
-
reconfiguredMembersAPI.bus.on('ready', function () {
|
|
46
|
-
membersApi = reconfiguredMembersAPI;
|
|
47
|
-
});
|
|
48
|
-
reconfiguredMembersAPI.bus.on('error', function (err) {
|
|
49
|
-
logging.error(err);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
38
|
/**
|
|
54
39
|
* @description Calculates threshold based on following formula
|
|
55
40
|
* Threshold = max{[current number of members], [volume threshold]}
|
|
@@ -57,7 +42,7 @@ function reconfigureMembersAPI() {
|
|
|
57
42
|
* @returns {Promise<number>}
|
|
58
43
|
*/
|
|
59
44
|
const fetchImportThreshold = async () => {
|
|
60
|
-
const membersTotal = await
|
|
45
|
+
const membersTotal = await module.exports.stats.getTotalMembers();
|
|
61
46
|
const configThreshold = _.get(config.get('hostSettings'), 'emailVerification.importThreshold');
|
|
62
47
|
const volumeThreshold = (configThreshold === undefined) ? Infinity : configThreshold;
|
|
63
48
|
const threshold = Math.max(membersTotal, volumeThreshold);
|
|
@@ -68,7 +53,7 @@ const fetchImportThreshold = async () => {
|
|
|
68
53
|
const membersImporter = new MembersCSVImporter({
|
|
69
54
|
storagePath: config.getContentPath('data'),
|
|
70
55
|
getTimezone: () => settingsCache.get('timezone'),
|
|
71
|
-
getMembersApi: () =>
|
|
56
|
+
getMembersApi: () => module.exports.api,
|
|
72
57
|
sendEmail: ghostMailer.send.bind(ghostMailer),
|
|
73
58
|
isSet: labsService.isSet.bind(labsService),
|
|
74
59
|
addJob: jobsService.addJob.bind(jobsService),
|
|
@@ -125,36 +110,14 @@ const processImport = async (options) => {
|
|
|
125
110
|
return result;
|
|
126
111
|
};
|
|
127
112
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Bind to events to automatically keep subscription info up-to-date from settings
|
|
131
|
-
events.on('settings.edited', function updateSettingFromModel(settingModel) {
|
|
132
|
-
if (![
|
|
133
|
-
'members_signup_access',
|
|
134
|
-
'members_from_address',
|
|
135
|
-
'members_support_address',
|
|
136
|
-
'members_reply_address',
|
|
137
|
-
'stripe_product_name',
|
|
138
|
-
'stripe_plans'
|
|
139
|
-
].includes(settingModel.get('key'))) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
debouncedReconfigureMembersAPI();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
events.on('services.stripe.reconfigured', reconfigureMembersAPI);
|
|
147
|
-
|
|
148
|
-
const membersService = {
|
|
113
|
+
module.exports = {
|
|
149
114
|
async init() {
|
|
115
|
+
const stripeService = require('../stripe');
|
|
116
|
+
const createMembersApiInstance = require('./api');
|
|
150
117
|
const env = config.get('env');
|
|
151
118
|
|
|
119
|
+
// @TODO Move to stripe service
|
|
152
120
|
if (env !== 'production') {
|
|
153
|
-
if (!process.env.WEBHOOK_SECRET && stripeService.api.configured) {
|
|
154
|
-
process.env.WEBHOOK_SECRET = 'DEFAULT_WEBHOOK_SECRET';
|
|
155
|
-
logging.warn(tpl(messages.remoteWebhooksInDevelopment));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
121
|
if (stripeService.api.configured && stripeService.api.mode === 'live') {
|
|
159
122
|
throw new errors.IncorrectUsageError({
|
|
160
123
|
message: tpl(messages.noLiveKeysInDevelopment)
|
|
@@ -175,6 +138,17 @@ const membersService = {
|
|
|
175
138
|
logging.error(err);
|
|
176
139
|
});
|
|
177
140
|
}
|
|
141
|
+
|
|
142
|
+
(async () => {
|
|
143
|
+
try {
|
|
144
|
+
const collection = await models.SingleUseToken.fetchAll();
|
|
145
|
+
await collection.invokeThen('destroy');
|
|
146
|
+
} catch (err) {
|
|
147
|
+
logging.error(err);
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
await stripeService.migrations.execute();
|
|
178
152
|
},
|
|
179
153
|
contentGating: require('./content-gating'),
|
|
180
154
|
|
|
@@ -196,7 +170,7 @@ const membersService = {
|
|
|
196
170
|
cookieKeys: [settingsCache.get('theme_session_secret')],
|
|
197
171
|
cookieName: 'ghost-members-ssr',
|
|
198
172
|
cookieCacheName: 'ghost-members-ssr-cache',
|
|
199
|
-
getMembersApi: () =>
|
|
173
|
+
getMembersApi: () => module.exports.api
|
|
200
174
|
}),
|
|
201
175
|
|
|
202
176
|
stripeConnect: require('./stripe-connect'),
|
|
@@ -208,7 +182,6 @@ const membersService = {
|
|
|
208
182
|
settingsCache: settingsCache,
|
|
209
183
|
isSQLite: config.get('database:client') === 'sqlite3'
|
|
210
184
|
})
|
|
211
|
-
};
|
|
212
185
|
|
|
213
|
-
|
|
186
|
+
};
|
|
214
187
|
module.exports.middleware = require('./middleware');
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
|
|
2
2
|
const OffersModule = require('@tryghost/members-offers');
|
|
3
3
|
|
|
4
|
-
const stripeService = require('../stripe');
|
|
5
|
-
|
|
6
4
|
const config = require('../../../shared/config');
|
|
7
5
|
const urlUtils = require('../../../shared/url-utils');
|
|
8
6
|
const models = require('../../models');
|
|
@@ -19,8 +17,7 @@ module.exports = {
|
|
|
19
17
|
const offersModule = OffersModule.create({
|
|
20
18
|
OfferModel: models.Offer,
|
|
21
19
|
OfferRedemptionModel: models.OfferRedemption,
|
|
22
|
-
redirectManager: redirectManager
|
|
23
|
-
stripeAPIService: stripeService.api
|
|
20
|
+
redirectManager: redirectManager
|
|
24
21
|
});
|
|
25
22
|
|
|
26
23
|
this.api = offersModule.api;
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
const {isPlainObject} = require('lodash');
|
|
2
2
|
const config = require('../../../shared/config');
|
|
3
3
|
const labs = require('../../../shared/labs');
|
|
4
|
+
const databaseInfo = require('../../data/db/info');
|
|
4
5
|
const ghostVersion = require('@tryghost/version');
|
|
5
6
|
|
|
6
7
|
module.exports = function getConfigProperties() {
|
|
7
8
|
const configProperties = {
|
|
8
9
|
version: ghostVersion.full,
|
|
9
10
|
environment: config.get('env'),
|
|
10
|
-
database:
|
|
11
|
+
database: databaseInfo.getEngine(),
|
|
11
12
|
mail: isPlainObject(config.get('mail')) ? config.get('mail').transport : '',
|
|
12
13
|
useGravatar: !config.isPrivacyDisabled('useGravatar'),
|
|
13
14
|
labs: labs.getAll(),
|
|
14
15
|
clientExtensions: config.get('clientExtensions') || {},
|
|
15
16
|
enableDeveloperExperiments: config.get('enableDeveloperExperiments') || false,
|
|
16
17
|
stripeDirect: config.get('stripeDirect'),
|
|
17
|
-
mailgunIsConfigured: config.get('bulkEmail') && config.get('bulkEmail').mailgun,
|
|
18
|
+
mailgunIsConfigured: !!(config.get('bulkEmail') && config.get('bulkEmail').mailgun),
|
|
18
19
|
emailAnalytics: config.get('emailAnalytics'),
|
|
19
20
|
hostSettings: config.get('hostSettings'),
|
|
20
21
|
tenor: config.get('tenor')
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
const
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const tpl = require('@tryghost/tpl');
|
|
2
3
|
|
|
4
|
+
const messages = {
|
|
5
|
+
remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// @TODO Refactor to a class w/ constructor
|
|
3
9
|
module.exports = {
|
|
4
|
-
getConfig(settings, config) {
|
|
10
|
+
getConfig(settings, config, urlUtils) {
|
|
5
11
|
/**
|
|
6
12
|
* @param {'direct' | 'connect'} type - The "type" of keys to fetch from settings
|
|
7
13
|
* @returns {{publicKey: string, secretKey: string} | null}
|
|
@@ -42,16 +48,25 @@ module.exports = {
|
|
|
42
48
|
if (!keys) {
|
|
43
49
|
return null;
|
|
44
50
|
}
|
|
51
|
+
|
|
52
|
+
const env = config.get('env');
|
|
53
|
+
let webhookSecret = process.env.WEBHOOK_SECRET;
|
|
54
|
+
|
|
55
|
+
if (env !== 'production') {
|
|
56
|
+
if (!webhookSecret) {
|
|
57
|
+
webhookSecret = 'DEFAULT_WEBHOOK_SECRET';
|
|
58
|
+
logging.warn(tpl(messages.remoteWebhooksInDevelopment));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const webhookHandlerUrl = new URL('members/webhooks/stripe/', urlUtils.getSiteUrl());
|
|
63
|
+
|
|
45
64
|
return {
|
|
46
65
|
secretKey: keys.secretKey,
|
|
47
66
|
publicKey: keys.publicKey,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
version: ghostVersion.original,
|
|
52
|
-
url: 'https://ghost.org/'
|
|
53
|
-
},
|
|
54
|
-
enablePromoCodes: config.get('enableStripePromoCodes')
|
|
67
|
+
enablePromoCodes: config.get('enableStripePromoCodes'),
|
|
68
|
+
webhookSecret: webhookSecret,
|
|
69
|
+
webhookHandlerUrl: webhookHandlerUrl.href
|
|
55
70
|
};
|
|
56
71
|
}
|
|
57
72
|
};
|
|
@@ -1,45 +1,53 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const
|
|
3
|
-
|
|
2
|
+
const StripeService = require('@tryghost/members-stripe-service');
|
|
3
|
+
const membersService = require('../members');
|
|
4
4
|
const config = require('../../../shared/config');
|
|
5
5
|
const settings = require('../../../shared/settings-cache');
|
|
6
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
6
7
|
const events = require('../../lib/common/events');
|
|
7
|
-
|
|
8
|
+
const models = require('../../models');
|
|
8
9
|
const {getConfig} = require('./config');
|
|
9
10
|
|
|
10
|
-
const api = new StripeAPIService({
|
|
11
|
-
config: {}
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const stripeKeySettings = [
|
|
15
|
-
'stripe_publishable_key',
|
|
16
|
-
'stripe_secret_key',
|
|
17
|
-
'stripe_connect_publishable_key',
|
|
18
|
-
'stripe_connect_secret_key'
|
|
19
|
-
];
|
|
20
|
-
|
|
21
11
|
function configureApi() {
|
|
22
|
-
const cfg = getConfig(settings, config);
|
|
12
|
+
const cfg = getConfig(settings, config, urlUtils);
|
|
23
13
|
if (cfg) {
|
|
24
|
-
|
|
14
|
+
module.exports.configure(cfg);
|
|
15
|
+
return true;
|
|
25
16
|
}
|
|
17
|
+
return false;
|
|
26
18
|
}
|
|
27
19
|
|
|
28
20
|
const debouncedConfigureApi = _.debounce(() => {
|
|
29
21
|
configureApi();
|
|
30
|
-
events.emit('services.stripe.reconfigured');
|
|
31
22
|
}, 600);
|
|
32
23
|
|
|
33
|
-
module.exports = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
module.exports = new StripeService({
|
|
25
|
+
membersService,
|
|
26
|
+
models: _.pick(models, ['Product', 'StripePrice', 'StripeCustomerSubscription', 'StripeProduct', 'MemberStripeCustomer', 'Offer', 'Settings']),
|
|
27
|
+
StripeWebhook: {
|
|
28
|
+
async get() {
|
|
29
|
+
return {
|
|
30
|
+
webhook_id: settings.get('members_stripe_webhook_id'),
|
|
31
|
+
secret: settings.get('members_stripe_webhook_secret')
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async save(data) {
|
|
35
|
+
await models.Settings.edit([{
|
|
36
|
+
key: 'members_stripe_webhook_id',
|
|
37
|
+
value: data.webhook_id
|
|
38
|
+
}, {
|
|
39
|
+
key: 'members_stripe_webhook_secret',
|
|
40
|
+
value: data.secret
|
|
41
|
+
}]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
module.exports.init = async function init() {
|
|
47
|
+
configureApi();
|
|
48
|
+
events.on('settings.edited', function (model) {
|
|
49
|
+
if (['stripe_publishable_key', 'stripe_secret_key', 'stripe_connect_publishable_key', 'stripe_connect_secret_key'].includes(model.get('key'))) {
|
|
50
|
+
debouncedConfigureApi();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
45
53
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('themes');
|
|
2
2
|
const bridge = require('../../../bridge');
|
|
3
|
-
const labs = require('../../../shared/labs');
|
|
4
3
|
const customThemeSettings = require('../custom-theme-settings');
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -11,25 +10,19 @@ module.exports = {
|
|
|
11
10
|
activateFromBoot: async (themeName, theme, checkedTheme) => {
|
|
12
11
|
debug('Activating theme (method A on boot)', themeName);
|
|
13
12
|
// TODO: probably a better place for this to happen - after successful activation / when reloading site?
|
|
14
|
-
|
|
15
|
-
await customThemeSettings.api.activateTheme(themeName, checkedTheme);
|
|
16
|
-
}
|
|
13
|
+
await customThemeSettings.api.activateTheme(themeName, checkedTheme);
|
|
17
14
|
await bridge.activateTheme(theme, checkedTheme);
|
|
18
15
|
},
|
|
19
16
|
activateFromAPI: async (themeName, theme, checkedTheme) => {
|
|
20
17
|
debug('Activating theme (method B on API "activate")', themeName);
|
|
21
18
|
// TODO: probably a better place for this to happen - after successful activation / when reloading site?
|
|
22
|
-
|
|
23
|
-
await customThemeSettings.api.activateTheme(themeName, checkedTheme);
|
|
24
|
-
}
|
|
19
|
+
await customThemeSettings.api.activateTheme(themeName, checkedTheme);
|
|
25
20
|
await bridge.activateTheme(theme, checkedTheme);
|
|
26
21
|
},
|
|
27
22
|
activateFromAPIOverride: async (themeName, theme, checkedTheme) => {
|
|
28
23
|
debug('Activating theme (method C on API "override")', themeName);
|
|
29
24
|
// TODO: probably a better place for this to happen - after successful activation / when reloading site?
|
|
30
|
-
|
|
31
|
-
await customThemeSettings.api.activateTheme(themeName, checkedTheme);
|
|
32
|
-
}
|
|
25
|
+
await customThemeSettings.api.activateTheme(themeName, checkedTheme);
|
|
33
26
|
await bridge.activateTheme(theme, checkedTheme);
|
|
34
27
|
}
|
|
35
28
|
};
|
|
@@ -6,12 +6,6 @@ const installer = require('./installer');
|
|
|
6
6
|
|
|
7
7
|
const settingsCache = require('../../../shared/settings-cache');
|
|
8
8
|
|
|
9
|
-
// Needed for theme re-activation after customThemeSettings flag is toggled
|
|
10
|
-
// @TODO: remove when customThemeSettings flag is removed
|
|
11
|
-
const labs = require('../../../shared/labs');
|
|
12
|
-
const events = require('../../lib/common/events');
|
|
13
|
-
let _lastLabsValue;
|
|
14
|
-
|
|
15
9
|
module.exports = {
|
|
16
10
|
/*
|
|
17
11
|
* Load the currently active theme
|
|
@@ -19,21 +13,6 @@ module.exports = {
|
|
|
19
13
|
init: async () => {
|
|
20
14
|
const themeName = settingsCache.get('active_theme');
|
|
21
15
|
|
|
22
|
-
/**
|
|
23
|
-
* When customThemeSettings labs flag is toggled we need to re-validate and activate
|
|
24
|
-
* the active theme so that it's settings are read and synced
|
|
25
|
-
*
|
|
26
|
-
* @TODO: remove when customThemeSettings labs flag is removed
|
|
27
|
-
*/
|
|
28
|
-
_lastLabsValue = labs.isSet('customThemeSettings');
|
|
29
|
-
events.on('settings.labs.edited', () => {
|
|
30
|
-
if (labs.isSet('customThemeSettings') !== _lastLabsValue) {
|
|
31
|
-
_lastLabsValue = labs.isSet('customThemeSettings');
|
|
32
|
-
|
|
33
|
-
activate.activate(settingsCache.get('active_theme'));
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
16
|
return activate.loadAndActivate(themeName);
|
|
38
17
|
},
|
|
39
18
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const {extract} = require('oembed-parser');
|
|
2
2
|
const logging = require('@tryghost/logging');
|
|
3
|
-
const labs = require('../../shared/labs');
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* @typedef {import('./oembed').ICustomProvider} ICustomProvider
|
|
@@ -43,7 +42,7 @@ class TwitterOEmbedProvider {
|
|
|
43
42
|
/** @type {object} */
|
|
44
43
|
const oembedData = await extract(url.href);
|
|
45
44
|
|
|
46
|
-
if (this.dependencies.config.bearerToken
|
|
45
|
+
if (this.dependencies.config.bearerToken) {
|
|
47
46
|
const query = {
|
|
48
47
|
expansions: ['attachments.poll_ids', 'attachments.media_keys', 'author_id', 'entities.mentions.username', 'geo.place_id', 'in_reply_to_user_id', 'referenced_tweets.id', 'referenced_tweets.id.author_id'],
|
|
49
48
|
'media.fields': ['duration_ms', 'height', 'media_key', 'preview_image_url', 'type', 'url', 'width', 'public_metrics', 'alt_text'],
|