ghost 5.114.1 → 5.115.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.114.1.tgz → tryghost-adapter-manager-5.115.1.tgz} +0 -0
- package/components/{tryghost-announcement-bar-settings-5.114.1.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
- package/components/{tryghost-api-framework-5.114.1.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
- package/components/tryghost-constants-5.115.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.114.1.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
- package/components/{tryghost-data-generator-5.114.1.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
- package/components/{tryghost-domain-events-5.114.1.tgz → tryghost-domain-events-5.115.1.tgz} +0 -0
- package/components/tryghost-donations-5.115.1.tgz +0 -0
- package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.114.1.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
- package/components/tryghost-email-events-5.115.1.tgz +0 -0
- package/components/tryghost-email-service-5.115.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
- package/components/tryghost-ghost-5.115.1.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.114.1.tgz → tryghost-html-to-plaintext-5.115.1.tgz} +0 -0
- package/components/tryghost-i18n-5.115.1.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
- package/components/{tryghost-job-manager-5.114.1.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.114.1.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
- package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
- package/components/{tryghost-magic-link-5.114.1.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
- package/components/{tryghost-mailgun-client-5.114.1.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
- package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
- package/components/{tryghost-member-events-5.114.1.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-api-5.114.1.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-csv-5.114.1.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-offers-5.114.1.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-payments-5.114.1.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
- package/components/{tryghost-milestones-5.114.1.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
- package/components/tryghost-minifier-5.115.1.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.114.1.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
- package/components/{tryghost-mw-version-match-5.114.1.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
- package/components/{tryghost-post-events-5.114.1.tgz → tryghost-post-events-5.115.1.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.114.1.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
- package/components/{tryghost-posts-service-5.114.1.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
- package/components/{tryghost-prometheus-metrics-5.114.1.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
- package/components/tryghost-recommendations-5.115.1.tgz +0 -0
- package/components/{tryghost-security-5.114.1.tgz → tryghost-security-5.115.1.tgz} +0 -0
- package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
- package/components/{tryghost-tiers-5.114.1.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
- package/components/{tryghost-webmentions-5.114.1.tgz → tryghost-webmentions-5.115.1.tgz} +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +11 -6
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/core/boot.js +3 -1
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
- package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
- package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
- package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
- package/core/built/admin/assets/{chunk.524.85c5b32bd46b91c147b9.js → chunk.524.2439684964c164c598ab.js} +7 -7
- package/core/built/admin/assets/{chunk.582.449a129a8005f03574bd.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
- package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
- package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
- package/core/built/admin/assets/{ghost-c563138cc2c0767bf6eefc9a2587eaa4.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +182 -160
- package/core/built/admin/assets/stats/stats.js +11824 -0
- package/core/built/admin/index.html +4 -4
- package/core/frontend/helpers/ghost_head.js +3 -1
- package/core/frontend/src/cards/css/cta.css +1 -1
- package/core/server/api/endpoints/slugs.js +6 -2
- package/core/server/data/importer/import-manager.js +2 -2
- package/core/server/data/importer/importers/importer-revue.js +128 -0
- package/core/server/data/importer/importers/json-to-html.js +107 -0
- package/core/server/data/migrations/utils/tables.js +2 -4
- package/core/server/data/migrations/versions/5.115/2025-03-24-07-19-27-add-identity-read-permission-to-administrators.js +6 -0
- package/core/server/data/schema/fixtures/fixtures.json +2 -1
- package/core/server/lib/bootstrap-socket.js +87 -0
- package/core/server/lib/package-json/index.js +1 -0
- package/core/server/lib/package-json/package-json.js +160 -0
- package/core/server/lib/package-json/parse.js +57 -0
- package/core/server/models/base/plugins/actions.js +44 -31
- package/core/server/models/base/plugins/generate-slug.js +6 -0
- package/core/server/notify.js +1 -1
- package/core/server/services/activitypub/ActivityPubService.ts +1 -1
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
- package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
- package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
- package/core/server/services/api-version-compatibility/index.js +2 -2
- package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
- package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
- package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
- package/core/server/services/audience-feedback/Feedback.js +35 -0
- package/core/server/services/audience-feedback/index.js +4 -2
- package/core/server/services/auth/session/emails/signin.js +168 -0
- package/core/server/services/auth/session/index.js +2 -2
- package/core/server/services/auth/session/session-from-token.js +69 -0
- package/core/server/services/auth/session/session-service.js +364 -0
- package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
- package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
- package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
- package/core/server/services/explore-ping/ExplorePingService.js +106 -0
- package/core/server/services/explore-ping/index.js +31 -0
- package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
- package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
- package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
- package/core/server/services/invitations/accept.js +5 -2
- package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
- package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
- package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
- package/core/server/services/mail-events/MailEvent.js +20 -0
- package/core/server/services/mail-events/MailEvent.ts +10 -0
- package/core/server/services/mail-events/MailEventRepository.js +2 -0
- package/core/server/services/mail-events/MailEventRepository.ts +5 -0
- package/core/server/services/mail-events/MailEventService.js +124 -0
- package/core/server/services/mail-events/MailEventService.ts +169 -0
- package/core/server/services/mail-events/index.js +1 -1
- package/core/server/services/mail-events/libraries.d.ts +2 -0
- package/core/server/services/members/CaptchaService.js +80 -0
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
- package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
- package/core/server/services/members/importer/email-template.js +182 -0
- package/core/server/services/members/importer/index.js +30 -0
- package/core/server/services/members/members-ssr.js +333 -0
- package/core/server/services/members/service.js +2 -2
- package/core/server/services/posts/stats/PostStats.js +13 -0
- package/core/server/services/route-settings/SettingsPathManager.js +47 -0
- package/core/server/services/route-settings/index.js +1 -1
- package/core/server/services/stripe/README.md +63 -0
- package/core/server/services/stripe/StripeAPI.js +931 -0
- package/core/server/services/stripe/StripeMigrations.js +613 -0
- package/core/server/services/stripe/StripeService.js +175 -0
- package/core/server/services/stripe/WebhookController.js +100 -0
- package/core/server/services/stripe/WebhookManager.js +175 -0
- package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
- package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
- package/core/server/services/stripe/events/index.js +4 -0
- package/core/server/services/stripe/service.js +1 -1
- package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
- package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
- package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
- package/core/server/services/themes/loader.js +1 -1
- package/core/server/services/themes/to-json.js +1 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/shared/middleware/cache-control.js +51 -0
- package/core/server/web/shared/middleware/index.js +1 -1
- package/core/server/web/well-known.js +1 -1
- package/core/shared/labs.js +3 -1
- package/core/shared/settings-cache/CacheManager.js +64 -6
- package/package.json +103 -134
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +7 -93
- package/components/tryghost-adapter-cache-redis-5.114.1.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.114.1.tgz +0 -0
- package/components/tryghost-audience-feedback-5.114.1.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.114.1.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.114.1.tgz +0 -0
- package/components/tryghost-captcha-service-5.114.1.tgz +0 -0
- package/components/tryghost-constants-5.114.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.114.1.tgz +0 -0
- package/components/tryghost-donations-5.114.1.tgz +0 -0
- package/components/tryghost-email-addresses-5.114.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.114.1.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.114.1.tgz +0 -0
- package/components/tryghost-email-events-5.114.1.tgz +0 -0
- package/components/tryghost-email-service-5.114.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.114.1.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.114.1.tgz +0 -0
- package/components/tryghost-extract-api-key-5.114.1.tgz +0 -0
- package/components/tryghost-ghost-5.114.1.tgz +0 -0
- package/components/tryghost-i18n-5.114.1.tgz +0 -0
- package/components/tryghost-identity-token-service-5.114.1.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.114.1.tgz +0 -0
- package/components/tryghost-importer-revue-5.114.1.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.114.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.114.1.tgz +0 -0
- package/components/tryghost-mail-events-5.114.1.tgz +0 -0
- package/components/tryghost-member-attribution-5.114.1.tgz +0 -0
- package/components/tryghost-members-importer-5.114.1.tgz +0 -0
- package/components/tryghost-members-ssr-5.114.1.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.114.1.tgz +0 -0
- package/components/tryghost-minifier-5.114.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.114.1.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.114.1.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.114.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.114.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.114.1.tgz +0 -0
- package/components/tryghost-package-json-5.114.1.tgz +0 -0
- package/components/tryghost-recommendations-5.114.1.tgz +0 -0
- package/components/tryghost-referrers-5.114.1.tgz +0 -0
- package/components/tryghost-session-service-5.114.1.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.114.1.tgz +0 -0
- package/components/tryghost-slack-notifications-5.114.1.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.114.1.tgz +0 -0
- package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
- package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const {DonationPaymentEvent} = require('@tryghost/donations');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
4
|
+
const logging = require('@tryghost/logging');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles `checkout.session.completed` webhook events
|
|
8
|
+
*
|
|
9
|
+
* The `checkout.session.completed` event is triggered when a customer completes a checkout session.
|
|
10
|
+
*
|
|
11
|
+
* It is triggered for the following scenarios:
|
|
12
|
+
* - Subscription
|
|
13
|
+
* - Donation
|
|
14
|
+
* - Setup intent
|
|
15
|
+
*
|
|
16
|
+
* This service delegates the event to the appropriate handler based on the session mode and metadata.
|
|
17
|
+
*
|
|
18
|
+
* The `session` payload can be found here: https://docs.stripe.com/api/checkout/sessions/object
|
|
19
|
+
*/
|
|
20
|
+
module.exports = class CheckoutSessionEventService {
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} deps
|
|
23
|
+
* @param {import('../../StripeAPI')} deps.api
|
|
24
|
+
* @param {object} deps.memberRepository
|
|
25
|
+
* @param {object} deps.donationRepository
|
|
26
|
+
* @param {object} deps.staffServiceEmails
|
|
27
|
+
* @param {function} deps.sendSignupEmail
|
|
28
|
+
*/
|
|
29
|
+
constructor(deps) {
|
|
30
|
+
this.api = deps.api;
|
|
31
|
+
this.deps = deps;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handles a `checkout.session.completed` event
|
|
36
|
+
* Delegates to the appropriate handler based on the session mode and metadata
|
|
37
|
+
* @param {import('stripe').Stripe.Checkout.Session} session
|
|
38
|
+
*/
|
|
39
|
+
async handleEvent(session) {
|
|
40
|
+
if (session.mode === 'setup') {
|
|
41
|
+
await this.handleSetupEvent(session);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (session.mode === 'subscription') {
|
|
45
|
+
await this.handleSubscriptionEvent(session);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (session.mode === 'payment' && session.metadata?.ghost_donation) {
|
|
49
|
+
await this.handleDonationEvent(session);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handles a `checkout.session.completed` event for a donation
|
|
55
|
+
* @param {import('stripe').Stripe.Checkout.Session} session
|
|
56
|
+
*/
|
|
57
|
+
async handleDonationEvent(session) {
|
|
58
|
+
const donationField = session.custom_fields?.find(obj => obj?.key === 'donation_message');
|
|
59
|
+
const donationMessage = donationField?.text?.value ? donationField.text.value : null;
|
|
60
|
+
const amount = session.amount_total;
|
|
61
|
+
const currency = session.currency;
|
|
62
|
+
|
|
63
|
+
const memberRepository = this.deps.memberRepository;
|
|
64
|
+
const member = session.customer ? (await memberRepository.get({customer_id: session.customer})) : null;
|
|
65
|
+
|
|
66
|
+
const data = DonationPaymentEvent.create({
|
|
67
|
+
name: member?.get('name') ?? session.customer_details.name,
|
|
68
|
+
email: member?.get('email') ?? session.customer_details.email,
|
|
69
|
+
memberId: member?.id ?? null,
|
|
70
|
+
amount,
|
|
71
|
+
currency,
|
|
72
|
+
donationMessage,
|
|
73
|
+
attributionId: session.metadata?.attribution_id ?? null,
|
|
74
|
+
attributionUrl: session.metadata?.attribution_url ?? null,
|
|
75
|
+
attributionType: session.metadata?.attribution_type ?? null,
|
|
76
|
+
referrerSource: session.metadata?.referrer_source ?? null,
|
|
77
|
+
referrerMedium: session.metadata?.referrer_medium ?? null,
|
|
78
|
+
referrerUrl: session.metadata?.referrer_url ?? null
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const donationRepository = this.deps.donationRepository;
|
|
82
|
+
await donationRepository.save(data);
|
|
83
|
+
|
|
84
|
+
const staffServiceEmails = this.deps.staffServiceEmails;
|
|
85
|
+
await staffServiceEmails.notifyDonationReceived({donationPaymentEvent: data});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handles a `checkout.session.completed` event for a setup intent
|
|
90
|
+
*
|
|
91
|
+
* This is used when a customer adds or changes their payment method outside
|
|
92
|
+
* of the normal subscription flow.
|
|
93
|
+
* @param {import('stripe').Stripe.Checkout.Session} session
|
|
94
|
+
*/
|
|
95
|
+
async handleSetupEvent(session) {
|
|
96
|
+
const setupIntent = await this.api.getSetupIntent(session.setup_intent);
|
|
97
|
+
|
|
98
|
+
const memberRepository = this.deps.memberRepository;
|
|
99
|
+
const member = await memberRepository.get({
|
|
100
|
+
customer_id: setupIntent.metadata.customer_id
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!member) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await this.api.attachPaymentMethodToCustomer(
|
|
108
|
+
setupIntent.metadata.customer_id,
|
|
109
|
+
setupIntent.payment_method
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (setupIntent.metadata.subscription_id) {
|
|
113
|
+
const updatedSubscription = await this.api.updateSubscriptionDefaultPaymentMethod(
|
|
114
|
+
setupIntent.metadata.subscription_id,
|
|
115
|
+
setupIntent.payment_method
|
|
116
|
+
);
|
|
117
|
+
try {
|
|
118
|
+
await memberRepository.linkSubscription({
|
|
119
|
+
id: member.id,
|
|
120
|
+
subscription: updatedSubscription
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
throw new errors.ConflictError({
|
|
127
|
+
err
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const subscriptions = await member.related('stripeSubscriptions').fetch();
|
|
134
|
+
const activeSubscriptions = subscriptions.models.filter(subscription => ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.get('status'))
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
for (const subscription of activeSubscriptions) {
|
|
138
|
+
if (subscription.get('customer_id') === setupIntent.metadata.customer_id) {
|
|
139
|
+
const updatedSubscription = await this.api.updateSubscriptionDefaultPaymentMethod(
|
|
140
|
+
subscription.get('subscription_id'),
|
|
141
|
+
setupIntent.payment_method
|
|
142
|
+
);
|
|
143
|
+
try {
|
|
144
|
+
await memberRepository.linkSubscription({
|
|
145
|
+
id: member.id,
|
|
146
|
+
subscription: updatedSubscription
|
|
147
|
+
});
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
throw new errors.ConflictError({
|
|
153
|
+
err
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handles a `checkout.session.completed` event for a subscription
|
|
162
|
+
* @param {import('stripe').Stripe.Checkout.Session} session
|
|
163
|
+
*/
|
|
164
|
+
async handleSubscriptionEvent(session) {
|
|
165
|
+
const customer = await this.api.getCustomer(session.customer, {
|
|
166
|
+
expand: ['subscriptions.data.default_payment_method']
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const memberRepository = this.deps.memberRepository;
|
|
170
|
+
|
|
171
|
+
let member = await memberRepository.get({
|
|
172
|
+
email: customer.email
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const checkoutType = _.get(session, 'metadata.checkoutType');
|
|
176
|
+
|
|
177
|
+
if (!member) {
|
|
178
|
+
const metadataName = _.get(session, 'metadata.name');
|
|
179
|
+
const metadataNewsletters = _.get(session, 'metadata.newsletters');
|
|
180
|
+
const attribution = {
|
|
181
|
+
id: session.metadata?.attribution_id ?? null,
|
|
182
|
+
url: session.metadata?.attribution_url ?? null,
|
|
183
|
+
type: session.metadata?.attribution_type ?? null,
|
|
184
|
+
referrerSource: session.metadata?.referrer_source ?? null,
|
|
185
|
+
referrerMedium: session.metadata?.referrer_medium ?? null,
|
|
186
|
+
referrerUrl: session.metadata?.referrer_url ?? null
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
|
|
190
|
+
const name = metadataName || payerName || null;
|
|
191
|
+
|
|
192
|
+
const memberData = {email: customer.email, name, attribution};
|
|
193
|
+
if (metadataNewsletters) {
|
|
194
|
+
try {
|
|
195
|
+
memberData.newsletters = JSON.parse(metadataNewsletters);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
logging.error(`Ignoring invalid newsletters data - ${metadataNewsletters}.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const offerId = session.metadata?.offer;
|
|
202
|
+
const memberDataWithStripeCustomer = {
|
|
203
|
+
...memberData,
|
|
204
|
+
stripeCustomer: customer,
|
|
205
|
+
offerId
|
|
206
|
+
};
|
|
207
|
+
member = await memberRepository.create(memberDataWithStripeCustomer);
|
|
208
|
+
} else {
|
|
209
|
+
const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
|
|
210
|
+
const attribution = {
|
|
211
|
+
id: session.metadata?.attribution_id ?? null,
|
|
212
|
+
url: session.metadata?.attribution_url ?? null,
|
|
213
|
+
type: session.metadata?.attribution_type ?? null,
|
|
214
|
+
referrerSource: session.metadata?.referrer_source ?? null,
|
|
215
|
+
referrerMedium: session.metadata?.referrer_medium ?? null,
|
|
216
|
+
referrerUrl: session.metadata?.referrer_url ?? null
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (payerName && !member.get('name')) {
|
|
220
|
+
await memberRepository.update({name: payerName}, {id: member.get('id')});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await memberRepository.upsertCustomer({
|
|
224
|
+
customer_id: customer.id,
|
|
225
|
+
member_id: member.id,
|
|
226
|
+
name: customer.name,
|
|
227
|
+
email: customer.email
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
for (const subscription of customer.subscriptions.data) {
|
|
231
|
+
try {
|
|
232
|
+
const offerId = session.metadata?.offer;
|
|
233
|
+
|
|
234
|
+
await memberRepository.linkSubscription({
|
|
235
|
+
id: member.id,
|
|
236
|
+
subscription,
|
|
237
|
+
offerId,
|
|
238
|
+
attribution
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
throw new errors.ConflictError({
|
|
245
|
+
err
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (checkoutType !== 'upgrade') {
|
|
252
|
+
this.deps.sendSignupEmail(customer.email);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
// const _ = require('lodash');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles `invoice.payment_succeeded` webhook events
|
|
6
|
+
*
|
|
7
|
+
* The `invoice.payment_succeeded` event is triggered when a customer's payment succeeds.
|
|
8
|
+
*/
|
|
9
|
+
module.exports = class InvoiceEventService {
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} deps
|
|
12
|
+
* @param {object} deps.api
|
|
13
|
+
* @param {object} deps.memberRepository
|
|
14
|
+
* @param {object} deps.eventRepository
|
|
15
|
+
* @param {object} deps.productRepository
|
|
16
|
+
*/
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.deps = deps;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handles a `invoice.payment_succeeded` event
|
|
23
|
+
*
|
|
24
|
+
* Inserts a payment event into the database
|
|
25
|
+
* @param {import('stripe').Stripe.Invoice} invoice
|
|
26
|
+
*/
|
|
27
|
+
async handleInvoiceEvent(invoice) {
|
|
28
|
+
const {api, memberRepository, eventRepository, productRepository} = this.deps;
|
|
29
|
+
|
|
30
|
+
if (!invoice.subscription) {
|
|
31
|
+
// Check if this is a one time payment, related to a donation
|
|
32
|
+
// this is being handled in checkoutSessionEvent because we need to handle the custom donation message
|
|
33
|
+
// which is not available in the invoice object
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const subscription = await api.getSubscription(invoice.subscription, {
|
|
37
|
+
expand: ['default_payment_method']
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const member = await memberRepository.get({
|
|
41
|
+
customer_id: subscription.customer
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (member) {
|
|
45
|
+
if (invoice.paid && invoice.amount_paid !== 0) {
|
|
46
|
+
await eventRepository.registerPayment({
|
|
47
|
+
member_id: member.id,
|
|
48
|
+
currency: invoice.currency,
|
|
49
|
+
amount: invoice.amount_paid
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
// Subscription has more than one plan - meaning it is not one created by us - ignore.
|
|
54
|
+
if (!subscription.plan) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Subscription is for a different product - ignore.
|
|
58
|
+
const product = await productRepository.get({
|
|
59
|
+
stripe_product_id: subscription.plan.product
|
|
60
|
+
});
|
|
61
|
+
if (!product) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Could not find the member, which we need in order to insert an payment event.
|
|
65
|
+
throw new errors.NotFoundError({
|
|
66
|
+
message: `No member found for customer ${subscription.customer}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles `customer.subscription.*` webhook events
|
|
6
|
+
*
|
|
7
|
+
* The `customer.subscription.*` events are triggered when a customer's subscription status changes.
|
|
8
|
+
*
|
|
9
|
+
* This service is responsible for handling these events and updating the subscription status in Ghost,
|
|
10
|
+
* although it mostly delegates the responsibility to the `MemberRepository`.
|
|
11
|
+
*/
|
|
12
|
+
module.exports = class SubscriptionEventService {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} deps
|
|
15
|
+
* @param {import('../../repositories/MemberRepository')} deps.memberRepository
|
|
16
|
+
*/
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.deps = deps;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handles a `customer.subscription.*` event
|
|
23
|
+
*
|
|
24
|
+
* Looks up the member by the Stripe customer ID and links the subscription to the member.
|
|
25
|
+
* @param {import('stripe').Stripe.Subscription} subscription
|
|
26
|
+
*/
|
|
27
|
+
async handleSubscriptionEvent(subscription) {
|
|
28
|
+
const subscriptionPriceData = _.get(subscription, 'items.data');
|
|
29
|
+
if (!subscriptionPriceData || subscriptionPriceData.length !== 1) {
|
|
30
|
+
throw new errors.BadRequestError({
|
|
31
|
+
message: 'Subscription should have exactly 1 price item'
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const memberRepository = this.deps.memberRepository;
|
|
36
|
+
const member = await memberRepository.get({
|
|
37
|
+
customer_id: subscription.customer
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (member) {
|
|
41
|
+
try {
|
|
42
|
+
await memberRepository.linkSubscription({
|
|
43
|
+
id: member.id,
|
|
44
|
+
subscription
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
throw new errors.ConflictError({err});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const themeList = require('./list');
|
|
3
3
|
const bridge = require('../../../bridge');
|
|
4
|
-
const packageJSON = require('
|
|
4
|
+
const packageJSON = require('../../lib/package-json');
|
|
5
5
|
const settingsCache = require('../../../shared/settings-cache');
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -168,6 +168,7 @@ module.exports = function apiRoutes() {
|
|
|
168
168
|
|
|
169
169
|
// ## Slugs
|
|
170
170
|
router.get('/slugs/:type/:name', mw.authAdminApi, http(api.slugs.generate));
|
|
171
|
+
router.get('/slugs/:type/:name/:id', mw.authAdminApi, http(api.slugs.generate));
|
|
171
172
|
|
|
172
173
|
// ## Themes
|
|
173
174
|
router.get('/themes/', mw.authAdminApi, http(api.themes.browse));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// # CacheControl Middleware
|
|
2
|
+
// Usage: cacheControl(profile), where profile is one of 'public' or 'private'
|
|
3
|
+
// After: checkIsPrivate
|
|
4
|
+
// Before: routes
|
|
5
|
+
// App: Admin|Site|API
|
|
6
|
+
//
|
|
7
|
+
// Allows each app to declare its own default caching rules
|
|
8
|
+
|
|
9
|
+
const isString = require('lodash/isString');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {'public'|'private'} profile Use "private" if you do not want caching
|
|
13
|
+
* @param {object} [options]
|
|
14
|
+
* @param {number} [options.maxAge] The max-age in seconds to use when profile is "public"
|
|
15
|
+
* @param {number} [options.staleWhileRevalidate] The stale-while-revalidate in seconds to use when profile is "public"
|
|
16
|
+
*/
|
|
17
|
+
const cacheControl = (profile, options = {maxAge: 0}) => {
|
|
18
|
+
const isOptionHasProperty = property => Object.prototype.hasOwnProperty.call(options, property);
|
|
19
|
+
const publicOptions = [
|
|
20
|
+
'public',
|
|
21
|
+
`max-age=${options.maxAge}`,
|
|
22
|
+
isOptionHasProperty('staleWhileRevalidate') ? `stale-while-revalidate=${options.staleWhileRevalidate}` : ''
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const profiles = {
|
|
26
|
+
public: publicOptions.filter(option => option).join(', '),
|
|
27
|
+
private: 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let output;
|
|
31
|
+
|
|
32
|
+
if (isString(profile) && Object.prototype.hasOwnProperty.call(profiles, profile)) {
|
|
33
|
+
output = profiles[profile];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {import('express').Request} req
|
|
38
|
+
* @param {import('express').Response} res
|
|
39
|
+
* @param {() => void} next
|
|
40
|
+
*
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
return function cacheControlHeaders(req, res, next) {
|
|
44
|
+
if (output) {
|
|
45
|
+
res.set({'Cache-Control': output});
|
|
46
|
+
}
|
|
47
|
+
next();
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = cacheControl;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const cacheControl = require('@tryghost/mw-cache-control');
|
|
2
1
|
const express = require('../../shared/express');
|
|
3
2
|
const settings = require('../../shared/settings-cache');
|
|
4
3
|
const config = require('../../shared/config');
|
|
4
|
+
const {cacheControl} = require('./shared/middleware');
|
|
5
5
|
|
|
6
6
|
module.exports = function setupWellKnownApp() {
|
|
7
7
|
const wellKnownApp = express('well-known');
|
package/core/shared/labs.js
CHANGED
|
@@ -11,6 +11,59 @@ const _ = require('lodash');
|
|
|
11
11
|
* - See the notes in core/server/lib/common/events
|
|
12
12
|
* - There's also a plan to introduce a proper caching layer, and rewrite this on top of that
|
|
13
13
|
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} PublicSettingsCache
|
|
17
|
+
* @property {string|null} title - The blog's title
|
|
18
|
+
* @property {string|null} description - The blog's description
|
|
19
|
+
* @property {string|null} logo - URL to the blog's logo
|
|
20
|
+
* @property {string|null} icon - URL to the blog's icon
|
|
21
|
+
* @property {string|null} accent_color - The blog's accent color
|
|
22
|
+
* @property {string|null} cover_image - URL to the blog's cover image
|
|
23
|
+
* @property {string|null} facebook - Facebook page name
|
|
24
|
+
* @property {string|null} twitter - Twitter username
|
|
25
|
+
* @property {string|null} lang - The blog's language code
|
|
26
|
+
* @property {string|null} locale - The blog's locale
|
|
27
|
+
* @property {string|null} timezone - The blog's timezone
|
|
28
|
+
* @property {string|null} codeinjection_head - Code injected into head
|
|
29
|
+
* @property {string|null} codeinjection_foot - Code injected into footer
|
|
30
|
+
* @property {string|null} navigation - JSON string of navigation items
|
|
31
|
+
* @property {string|null} secondary_navigation - JSON string of secondary navigation items
|
|
32
|
+
* @property {string|null} meta_title - Custom meta title
|
|
33
|
+
* @property {string|null} meta_description - Custom meta description
|
|
34
|
+
* @property {string|null} og_image - Open Graph image URL
|
|
35
|
+
* @property {string|null} og_title - Open Graph title
|
|
36
|
+
* @property {string|null} og_description - Open Graph description
|
|
37
|
+
* @property {string|null} twitter_image - Twitter card image URL
|
|
38
|
+
* @property {string|null} twitter_title - Twitter card title
|
|
39
|
+
* @property {string|null} twitter_description - Twitter card description
|
|
40
|
+
* @property {string|null} members_support_address - Support email for members
|
|
41
|
+
* @property {boolean|null} members_enabled - Whether members feature is enabled
|
|
42
|
+
* @property {boolean|null} allow_self_signup - Whether self signup is allowed
|
|
43
|
+
* @property {boolean|null} members_invite_only - Whether membership is invite only
|
|
44
|
+
* @property {string|null} members_signup_access - Member signup access level
|
|
45
|
+
* @property {boolean|null} paid_members_enabled - Whether paid memberships are enabled
|
|
46
|
+
* @property {string|null} firstpromoter_account - FirstPromoter account ID
|
|
47
|
+
* @property {string|null} portal_button_style - Portal button style
|
|
48
|
+
* @property {string|null} portal_button_signup_text - Portal signup button text
|
|
49
|
+
* @property {string|null} portal_button_icon - Portal button icon
|
|
50
|
+
* @property {string|null} portal_signup_terms_html - Portal signup terms HTML
|
|
51
|
+
* @property {boolean|null} portal_signup_checkbox_required - Whether signup checkbox is required
|
|
52
|
+
* @property {string|null} portal_plans - JSON string of available portal plans
|
|
53
|
+
* @property {string|null} portal_default_plan - Default portal plan
|
|
54
|
+
* @property {boolean|null} portal_name - Whether to show portal names
|
|
55
|
+
* @property {boolean|null} portal_button - Whether to show the portal button
|
|
56
|
+
* @property {boolean|null} comments_enabled - Whether comments are enabled
|
|
57
|
+
* @property {boolean|null} recommendations_enabled - Whether recommendations are enabled
|
|
58
|
+
* @property {boolean|null} outbound_link_tagging - Whether outbound link tagging is enabled
|
|
59
|
+
* @property {string|null} default_email_address - Default email address
|
|
60
|
+
* @property {string|null} support_email_address - Support email address
|
|
61
|
+
* @property {string|null} editor_default_email_recipients - Default email recipients for editor
|
|
62
|
+
* @property {boolean|null} captcha_enabled - Whether captcha is enabled
|
|
63
|
+
* @property {string|null} labs - JSON string of enabled labs features
|
|
64
|
+
* @property {never} [x] - Prevent accessing undefined properties
|
|
65
|
+
*/
|
|
66
|
+
|
|
14
67
|
class CacheManager {
|
|
15
68
|
/**
|
|
16
69
|
* @prop {Object} options
|
|
@@ -164,14 +217,19 @@ class CacheManager {
|
|
|
164
217
|
/**
|
|
165
218
|
* Get all the publicly accessible cache entries with their correct names
|
|
166
219
|
* Uses clone to prevent modifications from being reflected
|
|
167
|
-
|
|
220
|
+
* @return {PublicSettingsCache} cache
|
|
168
221
|
*/
|
|
169
222
|
getPublic() {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
223
|
+
// This block correctly builds the type signature for the return value
|
|
224
|
+
/** @type {PublicSettingsCache} */
|
|
225
|
+
let settings = Object.fromEntries(
|
|
226
|
+
Object.keys(this.publicSettings).map(key => [this.publicSettings[key], null])
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// This block correctly populates the values from the cache
|
|
230
|
+
for (const newKey in this.publicSettings) {
|
|
231
|
+
settings[newKey] = this._doGet(this.publicSettings[newKey]) ?? null;
|
|
232
|
+
}
|
|
175
233
|
|
|
176
234
|
return settings;
|
|
177
235
|
}
|