ghost 5.89.6 → 5.90.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-cache-memory-ttl-5.90.0.tgz +0 -0
- package/components/{tryghost-adapter-cache-redis-5.89.6.tgz → tryghost-adapter-cache-redis-5.90.0.tgz} +0 -0
- package/components/{tryghost-adapter-manager-5.89.6.tgz → tryghost-adapter-manager-5.90.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.90.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.89.6.tgz → tryghost-api-framework-5.90.0.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.90.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.90.0.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.90.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.90.0.tgz +0 -0
- package/components/tryghost-collections-5.90.0.tgz +0 -0
- package/components/tryghost-constants-5.90.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.90.0.tgz +0 -0
- package/components/{tryghost-data-generator-5.89.6.tgz → tryghost-data-generator-5.90.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.90.0.tgz +0 -0
- package/components/tryghost-donations-5.90.0.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.90.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.90.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.90.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.90.0.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.89.6.tgz → tryghost-email-content-generator-5.90.0.tgz} +0 -0
- package/components/tryghost-email-events-5.90.0.tgz +0 -0
- package/components/{tryghost-email-service-5.89.6.tgz → tryghost-email-service-5.90.0.tgz} +0 -0
- package/components/tryghost-email-suppression-list-5.90.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.90.0.tgz +0 -0
- package/components/tryghost-external-media-inliner-5.90.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.90.0.tgz +0 -0
- package/components/tryghost-ghost-5.90.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.90.0.tgz +0 -0
- package/components/tryghost-i18n-5.90.0.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.90.0.tgz +0 -0
- package/components/{tryghost-importer-revue-5.89.6.tgz → tryghost-importer-revue-5.90.0.tgz} +0 -0
- package/components/tryghost-in-memory-repository-5.90.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.89.6.tgz → tryghost-job-manager-5.90.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.90.0.tgz +0 -0
- package/components/{tryghost-link-replacer-5.89.6.tgz → tryghost-link-replacer-5.90.0.tgz} +0 -0
- package/components/{tryghost-link-tracking-5.89.6.tgz → tryghost-link-tracking-5.90.0.tgz} +0 -0
- package/components/tryghost-magic-link-5.90.0.tgz +0 -0
- package/components/tryghost-mail-events-5.90.0.tgz +0 -0
- package/components/{tryghost-mailgun-client-5.89.6.tgz → tryghost-mailgun-client-5.90.0.tgz} +0 -0
- package/components/{tryghost-member-attribution-5.89.6.tgz → tryghost-member-attribution-5.90.0.tgz} +0 -0
- package/components/tryghost-member-events-5.90.0.tgz +0 -0
- package/components/tryghost-members-api-5.90.0.tgz +0 -0
- package/components/tryghost-members-csv-5.90.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.90.0.tgz +0 -0
- package/components/tryghost-members-importer-5.90.0.tgz +0 -0
- package/components/tryghost-members-offers-5.90.0.tgz +0 -0
- package/components/tryghost-members-payments-5.90.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.90.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.90.0.tgz +0 -0
- package/components/tryghost-mentions-email-report-5.90.0.tgz +0 -0
- package/components/tryghost-milestones-5.90.0.tgz +0 -0
- package/components/tryghost-minifier-5.90.0.tgz +0 -0
- package/components/tryghost-model-to-domain-event-interceptor-5.90.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.90.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.90.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.90.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.90.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.90.0.tgz +0 -0
- package/components/tryghost-mw-version-match-5.90.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.90.0.tgz +0 -0
- package/components/tryghost-nql-filter-expansions-5.90.0.tgz +0 -0
- package/components/{tryghost-oembed-service-5.89.6.tgz → tryghost-oembed-service-5.90.0.tgz} +0 -0
- package/components/tryghost-package-json-5.90.0.tgz +0 -0
- package/components/{tryghost-post-events-5.89.6.tgz → tryghost-post-events-5.90.0.tgz} +0 -0
- package/components/tryghost-post-revisions-5.90.0.tgz +0 -0
- package/components/tryghost-posts-service-5.90.0.tgz +0 -0
- package/components/tryghost-recommendations-5.90.0.tgz +0 -0
- package/components/tryghost-referrers-5.90.0.tgz +0 -0
- package/components/tryghost-security-5.90.0.tgz +0 -0
- package/components/{tryghost-session-service-5.89.6.tgz → tryghost-session-service-5.90.0.tgz} +0 -0
- package/components/tryghost-settings-path-manager-5.90.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.90.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.89.6.tgz → tryghost-staff-service-5.90.0.tgz} +0 -0
- package/components/{tryghost-stats-service-5.89.6.tgz → tryghost-stats-service-5.90.0.tgz} +0 -0
- package/components/{tryghost-tiers-5.89.6.tgz → tryghost-tiers-5.90.0.tgz} +0 -0
- package/components/tryghost-update-check-service-5.90.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.90.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.90.0.tgz +0 -0
- package/components/tryghost-webmentions-5.90.0.tgz +0 -0
- package/core/boot.js +8 -5
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
- package/core/built/admin/assets/admin-x-activitypub/{index-e318fbcd.mjs → index-79b91eda.mjs} +4643 -4540
- package/core/built/admin/assets/admin-x-activitypub/modals-25d834a0.mjs +1017 -0
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
- package/core/built/admin/assets/admin-x-demo/index-9379b3eb.mjs +15889 -0
- package/core/built/admin/assets/admin-x-demo/{modals-c17f9071.mjs → modals-88dc270f.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-511c0703.mjs → CodeEditorView-821d5b2c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-5941f1da.mjs → index-1c47835f.mjs} +16355 -12569
- package/core/built/admin/assets/admin-x-settings/{index-a33217a0.mjs → index-29d842f9.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-434ac8fd.mjs → modals-7c030639.mjs} +6330 -5814
- package/core/built/admin/assets/{chunk.42.90f279da5ca389af68b9.js → chunk.42.eae83e03ed76c8e8c57a.js} +4 -4
- package/core/built/admin/assets/{chunk.524.c121fa5c220befde8234.js → chunk.524.4551f9a844deaa462046.js} +5 -5
- package/core/built/admin/assets/{chunk.582.78f1ec44751f5313fcc9.js → chunk.582.1b3f6a148cd1896c12aa.js} +6 -6
- package/core/built/admin/assets/ghost-dark-cedf65ab1ef5082419d66cc99a151071.css +1 -0
- package/core/built/admin/assets/ghost-e452bcc2710cf5a8372a7f7202661191.css +1 -0
- package/core/built/admin/assets/{ghost-532c38e4c3ecd17d68cdcc9fb58b9098.js → ghost-e733775ba236c045dbcdaaa39231c6d8.js} +61 -53
- package/core/built/admin/assets/{vendor-eb806b4d193377b9b4bbf885840bfdc2.js → vendor-bff75d127fcf4a75c8a83b473bdb308f.js} +2 -2
- package/core/built/admin/index.html +6 -6
- package/core/frontend/services/routing/RouterManager.js +7 -7
- package/core/frontend/src/cards/css/audio.css +3 -0
- package/core/frontend/src/cards/css/file.css +4 -1
- package/core/frontend/src/cards/css/product.css +3 -3
- package/core/frontend/src/cards/css/toggle.css +1 -1
- package/core/server/data/migrations/versions/5.90/2024-08-20-09-40-24-update-default-donations-suggested-amount.js +42 -0
- package/core/server/data/schema/default-settings/default-settings.json +1 -1
- package/core/server/models/base/plugins/crud.js +14 -0
- package/core/server/models/member-click-event.js +5 -2
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +37 -8
- package/core/server/services/email-analytics/lib/queries.js +117 -21
- package/core/server/services/members/RequestIntegrityTokenProvider.js +62 -0
- package/core/server/services/members/middleware.js +42 -2
- package/core/server/services/members/service.js +6 -0
- package/core/server/web/members/app.js +3 -0
- package/core/shared/instrumentation.js +39 -27
- package/package.json +150 -153
- package/yarn.lock +466 -801
- package/components/tryghost-adapter-cache-memory-ttl-5.89.6.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.89.6.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.89.6.tgz +0 -0
- package/components/tryghost-audience-feedback-5.89.6.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.89.6.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.89.6.tgz +0 -0
- package/components/tryghost-collections-5.89.6.tgz +0 -0
- package/components/tryghost-constants-5.89.6.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.89.6.tgz +0 -0
- package/components/tryghost-domain-events-5.89.6.tgz +0 -0
- package/components/tryghost-donations-5.89.6.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.89.6.tgz +0 -0
- package/components/tryghost-email-addresses-5.89.6.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.89.6.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.89.6.tgz +0 -0
- package/components/tryghost-email-events-5.89.6.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.89.6.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.89.6.tgz +0 -0
- package/components/tryghost-external-media-inliner-5.89.6.tgz +0 -0
- package/components/tryghost-extract-api-key-5.89.6.tgz +0 -0
- package/components/tryghost-ghost-5.89.6.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.89.6.tgz +0 -0
- package/components/tryghost-i18n-5.89.6.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.89.6.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.89.6.tgz +0 -0
- package/components/tryghost-link-redirects-5.89.6.tgz +0 -0
- package/components/tryghost-magic-link-5.89.6.tgz +0 -0
- package/components/tryghost-mail-events-5.89.6.tgz +0 -0
- package/components/tryghost-member-events-5.89.6.tgz +0 -0
- package/components/tryghost-members-api-5.89.6.tgz +0 -0
- package/components/tryghost-members-csv-5.89.6.tgz +0 -0
- package/components/tryghost-members-events-service-5.89.6.tgz +0 -0
- package/components/tryghost-members-importer-5.89.6.tgz +0 -0
- package/components/tryghost-members-offers-5.89.6.tgz +0 -0
- package/components/tryghost-members-payments-5.89.6.tgz +0 -0
- package/components/tryghost-members-ssr-5.89.6.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.89.6.tgz +0 -0
- package/components/tryghost-mentions-email-report-5.89.6.tgz +0 -0
- package/components/tryghost-milestones-5.89.6.tgz +0 -0
- package/components/tryghost-minifier-5.89.6.tgz +0 -0
- package/components/tryghost-model-to-domain-event-interceptor-5.89.6.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.89.6.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.89.6.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.89.6.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.89.6.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.89.6.tgz +0 -0
- package/components/tryghost-mw-version-match-5.89.6.tgz +0 -0
- package/components/tryghost-mw-vhost-5.89.6.tgz +0 -0
- package/components/tryghost-nql-filter-expansions-5.89.6.tgz +0 -0
- package/components/tryghost-package-json-5.89.6.tgz +0 -0
- package/components/tryghost-post-revisions-5.89.6.tgz +0 -0
- package/components/tryghost-posts-service-5.89.6.tgz +0 -0
- package/components/tryghost-recommendations-5.89.6.tgz +0 -0
- package/components/tryghost-referrers-5.89.6.tgz +0 -0
- package/components/tryghost-security-5.89.6.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.89.6.tgz +0 -0
- package/components/tryghost-slack-notifications-5.89.6.tgz +0 -0
- package/components/tryghost-update-check-service-5.89.6.tgz +0 -0
- package/components/tryghost-verification-trigger-5.89.6.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.89.6.tgz +0 -0
- package/components/tryghost-webmentions-5.89.6.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/modals-a6e383d4.mjs +0 -422
- package/core/built/admin/assets/admin-x-demo/index-116afae1.mjs +0 -12365
- package/core/built/admin/assets/ghost-dark-f9c5f2aae7d01ae36bd6932c0abf683a.css +0 -1
- package/core/built/admin/assets/ghost-f627cc6acd7574a7bdbf2f2988365c78.css +0 -1
- /package/core/built/admin/assets/{chunk.42.90f279da5ca389af68b9.js.LICENSE.txt → chunk.42.eae83e03ed76c8e8c57a.js.LICENSE.txt} +0 -0
|
@@ -8782,7 +8782,7 @@ e.default={content:'<path stroke="currentColor" stroke-linecap="round" stroke-li
|
|
|
8782
8782
|
Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0
|
|
8783
8783
|
e.default={content:'<defs><style>.labs_svg__a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px}</style></defs><path class="labs_svg__a" d="M6.726.75h10.5m-1.5 7.5V.75h-7.5v7.5L1.489 18.615A3 3 0 004 23.25h15.948a3 3 0 002.515-4.635zm-10.425 4.5h13.35m-4.425 4.5h3m-1.5-1.5v3"/><path class="labs_svg__a" d="M6.726 19.125a.375.375 0 01.374.375m-.749 0a.375.375 0 01.375-.375m0 .75a.375.375 0 01-.375-.375"/><path class="labs_svg__a" d="M7.1 19.5a.375.375 0 01-.375.375m3.001-3.75a.375.375 0 01.375.375m-.75 0a.375.375 0 01.375-.375m0 .75a.375.375 0 01-.375-.375"/><path class="labs_svg__a" d="M10.1 16.5a.375.375 0 01-.375.375M15.726 3.75h-3m3 3h-3"/>',attrs:{viewBox:"0 0 24 24"}}})),define("ember-svg-jar/inlined/link",["exports"],(function(e){"use strict"
|
|
8784
8784
|
Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0
|
|
8785
|
-
e.default={content:'<path
|
|
8785
|
+
e.default={content:'<desc>Hyperlink 3 Streamline Icon: https://streamlinehq.com</desc><path d="M9.364 18.5l-.932.932a4.5 4.5 0 01-6.364-6.364l4.773-4.774a4.5 4.5 0 016.825 5.825" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M14.818 5.567l.75-.75a4.5 4.5 0 016.364 6.364l-4.773 4.773a4.5 4.5 0 01-6.824-5.826" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/>',attrs:{viewBox:"0 0 24 24"}}})),define("ember-svg-jar/inlined/lock-filled",["exports"],(function(e){"use strict"
|
|
8786
8786
|
Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0
|
|
8787
8787
|
e.default={content:'<path d="M19.5 9.5h-.75V6.75a6.75 6.75 0 00-13.5 0V9.5H4.5a2 2 0 00-2 2V22a2 2 0 002 2h15a2 2 0 002-2V11.5a2 2 0 00-2-2zm-7.5 9a2 2 0 112-2 2 2 0 01-2 2zM16.25 9a.5.5 0 01-.5.5h-7.5a.5.5 0 01-.5-.5V6.75a4.25 4.25 0 018.5 0z" fill="currentColor"/>',attrs:{viewBox:"0 0 24 24"}}})),define("ember-svg-jar/inlined/lock",["exports"],(function(e){"use strict"
|
|
8788
8788
|
Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0
|
|
@@ -9518,4 +9518,4 @@ e.default=class{constructor(e){if(this._data=new t.default,e)for(let t=0;t<e.len
|
|
|
9518
9518
|
return this}get(e){let t=this._data[e]
|
|
9519
9519
|
return t===r.UNDEFINED_KEY?void 0:t}set(e,t){return this._data[e]=t,this}delete(e){return this._data[e]=r.UNDEFINED_KEY,!0}}}))
|
|
9520
9520
|
|
|
9521
|
-
//# sourceMappingURL=vendor-
|
|
9521
|
+
//# sourceMappingURL=vendor-a3242d3bb7bb2f2ad87cc8ba3d607097.map
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Ghost Admin</title>
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.
|
|
11
|
+
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.90%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%224e5636b008%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%22b027d76bc5%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%2206c6b7a1d5%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%2261ea0f97da%22%7D" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</style>
|
|
38
38
|
|
|
39
39
|
<link integrity="" rel="stylesheet" href="assets/vendor-0ede59da8efb5e28fa929557f7ff7154.css">
|
|
40
|
-
<link integrity="" rel="stylesheet" href="assets/ghost-
|
|
40
|
+
<link integrity="" rel="stylesheet" href="assets/ghost-e452bcc2710cf5a8372a7f7202661191.css" title="light">
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
</head>
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
|
|
57
57
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
58
58
|
|
|
59
|
-
<script src="assets/vendor-
|
|
60
|
-
<script src="assets/chunk.42.
|
|
61
|
-
<script src="assets/chunk.524.
|
|
62
|
-
<script src="assets/ghost-
|
|
59
|
+
<script src="assets/vendor-bff75d127fcf4a75c8a83b473bdb308f.js"></script>
|
|
60
|
+
<script src="assets/chunk.42.eae83e03ed76c8e8c57a.js"></script>
|
|
61
|
+
<script src="assets/chunk.524.4551f9a844deaa462046.js"></script>
|
|
62
|
+
<script src="assets/ghost-e733775ba236c045dbcdaaa39231c6d8.js"></script>
|
|
63
63
|
</body>
|
|
64
64
|
</html>
|
|
@@ -126,6 +126,13 @@ class RouterManager {
|
|
|
126
126
|
this.registry.setRouter(staticRoutesRouter.identifier, staticRoutesRouter);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
_.each(routerSettings.taxonomies, (value, key) => {
|
|
130
|
+
const taxonomyRouter = new TaxonomyRouter(key, value, RESOURCE_CONFIG, this.routerCreated.bind(this));
|
|
131
|
+
this.siteRouter.mountRouter(taxonomyRouter.router());
|
|
132
|
+
|
|
133
|
+
this.registry.setRouter(taxonomyRouter.identifier, taxonomyRouter);
|
|
134
|
+
});
|
|
135
|
+
|
|
129
136
|
_.each(routerSettings.collections, (value, key) => {
|
|
130
137
|
const collectionRouter = new CollectionRouter(key, value, RESOURCE_CONFIG, this.routerCreated.bind(this));
|
|
131
138
|
this.siteRouter.mountRouter(collectionRouter.router());
|
|
@@ -137,13 +144,6 @@ class RouterManager {
|
|
|
137
144
|
|
|
138
145
|
this.registry.setRouter('staticPagesRouter', staticPagesRouter);
|
|
139
146
|
|
|
140
|
-
_.each(routerSettings.taxonomies, (value, key) => {
|
|
141
|
-
const taxonomyRouter = new TaxonomyRouter(key, value, RESOURCE_CONFIG, this.routerCreated.bind(this));
|
|
142
|
-
this.siteRouter.mountRouter(taxonomyRouter.router());
|
|
143
|
-
|
|
144
|
-
this.registry.setRouter(taxonomyRouter.identifier, taxonomyRouter);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
147
|
const appRouter = new ParentRouter('AppsRouter');
|
|
148
148
|
this.siteRouter.mountRouter(appRouter.router());
|
|
149
149
|
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
min-height: 96px;
|
|
10
10
|
border-radius: 6px;
|
|
11
11
|
padding: 4px;
|
|
12
|
+
background: #fff;
|
|
13
|
+
color: #222;
|
|
12
14
|
box-shadow: inset 0 0 0 1px rgba(124, 139, 154, 0.25);
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
.kg-audio-card+.kg-audio-card {
|
|
@@ -14,11 +14,14 @@
|
|
|
14
14
|
color: inherit;
|
|
15
15
|
padding: 12px;
|
|
16
16
|
min-height: 92px;
|
|
17
|
+
background: #fff;
|
|
18
|
+
color: #222;
|
|
17
19
|
border: 1px solid rgb(124 139 154 / 25%);
|
|
18
20
|
border-radius: 5px;
|
|
19
21
|
transition: all ease-in-out 0.35s;
|
|
20
22
|
text-decoration: none;
|
|
21
23
|
width: 100%;
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
.kg-file-card a.kg-file-card-container:hover {
|
|
@@ -137,4 +140,4 @@
|
|
|
137
140
|
|
|
138
141
|
.kg-file-card + .kg-file-card {
|
|
139
142
|
margin-top: 1em;
|
|
140
|
-
}
|
|
143
|
+
}
|
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
max-width: 550px;
|
|
20
20
|
padding: 20px;
|
|
21
21
|
width: 100%;
|
|
22
|
+
background: #fff;
|
|
23
|
+
color: #222;
|
|
22
24
|
border-radius: 5px;
|
|
23
25
|
box-shadow: inset 0 0 0 1px rgb(124 139 154 / 25%);
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
.kg-product-card-image {
|
|
@@ -34,7 +37,6 @@
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
.kg-product-card h4.kg-product-card-title {
|
|
37
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
38
40
|
text-decoration: none;
|
|
39
41
|
font-weight: 600;
|
|
40
42
|
font-size: 21px;
|
|
@@ -50,7 +52,6 @@
|
|
|
50
52
|
.kg-product-card .kg-product-card-description p,
|
|
51
53
|
.kg-product-card .kg-product-card-description ol,
|
|
52
54
|
.kg-product-card .kg-product-card-description ul {
|
|
53
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
54
55
|
font-size: 14px;
|
|
55
56
|
line-height: 1.5em;
|
|
56
57
|
opacity: .7;
|
|
@@ -115,7 +116,6 @@
|
|
|
115
116
|
display: flex;
|
|
116
117
|
position: static;
|
|
117
118
|
align-items: center;
|
|
118
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
119
119
|
font-size: 14px;
|
|
120
120
|
font-weight: 600;
|
|
121
121
|
line-height: 1em;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
|
|
2
|
+
|
|
3
|
+
const logging = require('@tryghost/logging');
|
|
4
|
+
|
|
5
|
+
// For DDL - schema changes
|
|
6
|
+
// const {createNonTransactionalMigration} = require('../../utils');
|
|
7
|
+
|
|
8
|
+
// For DML - data changes
|
|
9
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
10
|
+
|
|
11
|
+
// Or use a specific helper
|
|
12
|
+
// const {addTable, createAddColumnMigration} = require('../../utils');
|
|
13
|
+
|
|
14
|
+
module.exports = createTransactionalMigration(
|
|
15
|
+
async function up(knex) {
|
|
16
|
+
try {
|
|
17
|
+
// find the existing donations_suggested_amount setting
|
|
18
|
+
const existingSuggestedAmount = await knex('settings')
|
|
19
|
+
.where({key: 'donations_suggested_amount'})
|
|
20
|
+
.first();
|
|
21
|
+
|
|
22
|
+
// previous default is '0', if it's been set to something else we don't want to change it
|
|
23
|
+
if (existingSuggestedAmount.value !== '0') {
|
|
24
|
+
logging.info('donations_suggested_amount setting does not have previous default of 0, skipping migration');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// new default is '500', update the setting
|
|
29
|
+
logging.info('Updating donations_suggested_amount default setting to 500');
|
|
30
|
+
await knex('settings')
|
|
31
|
+
.where({key: 'donations_suggested_amount'})
|
|
32
|
+
.update({value: '500'});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logging.error(`Error updating donations_suggested_amount setting: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
async function down() {
|
|
38
|
+
// no-op
|
|
39
|
+
// we can't guarantee that a suggested amount of 500 now isn't
|
|
40
|
+
// something that was set explicitly
|
|
41
|
+
}
|
|
42
|
+
);
|
|
@@ -120,6 +120,20 @@ module.exports = function (Bookshelf) {
|
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
if (Array.isArray(options.cte)) {
|
|
124
|
+
options.cte.forEach((cte) => {
|
|
125
|
+
itemCollection.query((qb) => {
|
|
126
|
+
qb.with(cte.name, qb.client.raw(cte.query));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (options.from) {
|
|
132
|
+
itemCollection.query((qb) => {
|
|
133
|
+
qb.from(options.from);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
//option param to skip distinct from count query, distinct adds a lot of latency and in this case the result set will always be unique.
|
|
124
138
|
if (unfilteredOptions.useBasicCount) {
|
|
125
139
|
options.useBasicCount = unfilteredOptions.useBasicCount;
|
|
@@ -21,7 +21,10 @@ const MemberClickEvent = ghostBookshelf.Model.extend({
|
|
|
21
21
|
return expansions;
|
|
22
22
|
},
|
|
23
23
|
|
|
24
|
-
filterRelations() {
|
|
24
|
+
filterRelations(options) {
|
|
25
|
+
if (options && options.filterRelations === false) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
25
28
|
return {
|
|
26
29
|
link: {
|
|
27
30
|
// Mongo-knex doesn't support belongsTo relations
|
|
@@ -47,7 +50,7 @@ const MemberClickEvent = ghostBookshelf.Model.extend({
|
|
|
47
50
|
permittedOptions(methodName) {
|
|
48
51
|
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
49
52
|
const validOptions = {
|
|
50
|
-
findPage: ['selectRaw', 'whereRaw']
|
|
53
|
+
findPage: ['selectRaw', 'whereRaw', 'cte', 'from', 'useCTE', 'filterRelations']
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
if (validOptions[methodName]) {
|
|
@@ -57,11 +57,22 @@ class EmailAnalyticsServiceWrapper {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
async
|
|
61
|
-
logging.info('[EmailAnalytics] Fetch latest started');
|
|
60
|
+
async fetchLatestOpenedEvents({maxEvents} = {maxEvents: Infinity}) {
|
|
61
|
+
logging.info('[EmailAnalytics] Fetch latest opened events started');
|
|
62
62
|
|
|
63
63
|
const fetchStartDate = new Date();
|
|
64
|
-
const totalEvents = await this.service.
|
|
64
|
+
const totalEvents = await this.service.fetchLatestOpenedEvents({maxEvents});
|
|
65
|
+
const fetchEndDate = new Date();
|
|
66
|
+
|
|
67
|
+
logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (latest opens)`);
|
|
68
|
+
return totalEvents;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async fetchLatestNonOpenedEvents({maxEvents} = {maxEvents: Infinity}) {
|
|
72
|
+
logging.info('[EmailAnalytics] Fetch latest non-opened events started');
|
|
73
|
+
|
|
74
|
+
const fetchStartDate = new Date();
|
|
75
|
+
const totalEvents = await this.service.fetchLatestNonOpenedEvents({maxEvents});
|
|
65
76
|
const fetchEndDate = new Date();
|
|
66
77
|
|
|
67
78
|
logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (latest)`);
|
|
@@ -69,7 +80,7 @@ class EmailAnalyticsServiceWrapper {
|
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
async fetchMissing({maxEvents} = {maxEvents: Infinity}) {
|
|
72
|
-
logging.info('[EmailAnalytics] Fetch missing started');
|
|
83
|
+
logging.info('[EmailAnalytics] Fetch missing events started');
|
|
73
84
|
|
|
74
85
|
const fetchStartDate = new Date();
|
|
75
86
|
const totalEvents = await this.service.fetchMissing({maxEvents});
|
|
@@ -83,7 +94,7 @@ class EmailAnalyticsServiceWrapper {
|
|
|
83
94
|
if (maxEvents < 300) {
|
|
84
95
|
return 0;
|
|
85
96
|
}
|
|
86
|
-
logging.info('[EmailAnalytics] Fetch scheduled started');
|
|
97
|
+
logging.info('[EmailAnalytics] Fetch scheduled events started');
|
|
87
98
|
|
|
88
99
|
const fetchStartDate = new Date();
|
|
89
100
|
const totalEvents = await this.service.fetchScheduled({maxEvents});
|
|
@@ -100,13 +111,31 @@ class EmailAnalyticsServiceWrapper {
|
|
|
100
111
|
}
|
|
101
112
|
this.fetching = true;
|
|
102
113
|
|
|
114
|
+
// NOTE: Data shows we can process ~7500 events per minute on Pro; this can vary locally
|
|
103
115
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
// Prioritize opens since they are the most important (only data directly displayed to users)
|
|
117
|
+
await this.fetchLatestOpenedEvents({maxEvents: Infinity});
|
|
118
|
+
|
|
119
|
+
// Set limits on how much we fetch without checkings for opened events. During surge events (following newsletter send)
|
|
120
|
+
// we want to make sure we don't spend too much time collecting delivery data.
|
|
121
|
+
const c1 = await this.fetchLatestNonOpenedEvents({maxEvents: 20000});
|
|
122
|
+
if (c1 > 15000) {
|
|
123
|
+
this.fetching = false;
|
|
124
|
+
logging.info('[EmailAnalytics] Restarting fetch due to high event count');
|
|
125
|
+
this.startFetch();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const c2 = await this.fetchMissing({maxEvents: 20000});
|
|
129
|
+
if ((c1 + c2) > 15000) {
|
|
130
|
+
this.fetching = false;
|
|
131
|
+
logging.info('[EmailAnalytics] Restarting fetch due to high event count');
|
|
132
|
+
this.startFetch();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
106
135
|
|
|
107
136
|
// Only fetch scheduled if we didn't fetch a lot of normal events
|
|
108
137
|
await this.fetchScheduled({maxEvents: 20000 - c1 - c2});
|
|
109
|
-
|
|
138
|
+
|
|
110
139
|
this.fetching = false;
|
|
111
140
|
} catch (e) {
|
|
112
141
|
logging.error(e, 'Error while fetching email analytics');
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('services:email-analytics');
|
|
3
3
|
const db = require('../../../data/db');
|
|
4
|
+
const logging = require('@tryghost/logging');
|
|
5
|
+
const {default: ObjectID} = require('bson-objectid');
|
|
4
6
|
|
|
5
7
|
const MIN_EMAIL_COUNT_FOR_OPEN_RATE = 5;
|
|
6
8
|
|
|
9
|
+
/** @typedef {'email-analytics-latest-opened'|'email-analytics-latest-others'|'email-analytics-missing'|'email-analytics-scheduled'} EmailAnalyticsJobName */
|
|
10
|
+
/** @typedef {'delivered'|'opened'|'failed'} EmailAnalyticsEvent */
|
|
11
|
+
|
|
7
12
|
module.exports = {
|
|
8
13
|
async shouldFetchStats() {
|
|
9
14
|
// don't fetch stats from Mailgun if we haven't sent any emails
|
|
@@ -11,35 +16,126 @@ module.exports = {
|
|
|
11
16
|
return emailCount && emailCount.count > 0;
|
|
12
17
|
},
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the timestamp of the last seen event for the specified email analytics events.
|
|
21
|
+
* @param {EmailAnalyticsJobName} jobName - The name of the job to update.
|
|
22
|
+
* @param {EmailAnalyticsEvent[]} [events=['delivered', 'opened', 'failed']] - The email analytics events to consider.
|
|
23
|
+
* @returns {Promise<Date|null>} The timestamp of the last seen event, or null if no events are found.
|
|
24
|
+
*/
|
|
25
|
+
async getLastEventTimestamp(jobName, events = ['delivered', 'opened', 'failed']) {
|
|
15
26
|
const startDate = new Date();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
let
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
|
|
28
|
+
let maxOpenedAt;
|
|
29
|
+
let maxDeliveredAt;
|
|
30
|
+
let maxFailedAt;
|
|
31
|
+
|
|
32
|
+
const jobData = await db.knex('jobs').select('finished_at', 'started_at').where('name', jobName).first();
|
|
33
|
+
|
|
34
|
+
if (jobData) {
|
|
35
|
+
debug(`Using job data for ${jobName}`);
|
|
36
|
+
const lastJobTimestamp = jobData.finished_at || jobData.started_at;
|
|
37
|
+
maxOpenedAt = events.includes('opened') ? lastJobTimestamp : null;
|
|
38
|
+
maxDeliveredAt = events.includes('delivered') ? lastJobTimestamp : null;
|
|
39
|
+
maxFailedAt = events.includes('failed') ? lastJobTimestamp : null;
|
|
40
|
+
} else {
|
|
41
|
+
debug(`Job data not found for ${jobName}, using email_recipients data`);
|
|
42
|
+
logging.info(`Job data not found for ${jobName}, using email_recipients data`);
|
|
43
|
+
if (events.includes('opened')) {
|
|
44
|
+
maxOpenedAt = (await db.knex('email_recipients').select(db.knex.raw('MAX(opened_at) as maxOpenedAt')).first()).maxOpenedAt;
|
|
45
|
+
}
|
|
46
|
+
if (events.includes('delivered')) {
|
|
47
|
+
maxDeliveredAt = (await db.knex('email_recipients').select(db.knex.raw('MAX(delivered_at) as maxDeliveredAt')).first()).maxDeliveredAt;
|
|
48
|
+
}
|
|
49
|
+
if (events.includes('failed')) {
|
|
50
|
+
maxFailedAt = (await db.knex('email_recipients').select(db.knex.raw('MAX(failed_at) as maxFailedAt')).first()).maxFailedAt;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Insert a new job row if it doesn't exist
|
|
54
|
+
await db.knex('jobs').insert({
|
|
55
|
+
id: new ObjectID().toHexString(),
|
|
56
|
+
name: jobName,
|
|
57
|
+
started_at: new Date(),
|
|
58
|
+
created_at: new Date(),
|
|
59
|
+
status: 'started'
|
|
60
|
+
}).onConflict('name').ignore();
|
|
25
61
|
}
|
|
26
62
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (maxFailedAt && !(maxFailedAt instanceof Date)) {
|
|
33
|
-
// SQLite returns a string instead of a Date
|
|
34
|
-
maxFailedAt = new Date(maxFailedAt);
|
|
35
|
-
}
|
|
63
|
+
// Convert string dates to Date objects for SQLite compatibility
|
|
64
|
+
[maxOpenedAt, maxDeliveredAt, maxFailedAt] = [maxOpenedAt, maxDeliveredAt, maxFailedAt].map(date => (
|
|
65
|
+
date && !(date instanceof Date) ? new Date(date) : date
|
|
66
|
+
));
|
|
36
67
|
|
|
37
|
-
const lastSeenEventTimestamp = _.max([
|
|
68
|
+
const lastSeenEventTimestamp = _.max([maxOpenedAt, maxDeliveredAt, maxFailedAt]);
|
|
38
69
|
debug(`getLastSeenEventTimestamp: finished in ${Date.now() - startDate}ms`);
|
|
39
70
|
|
|
40
71
|
return lastSeenEventTimestamp;
|
|
41
72
|
},
|
|
42
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Sets the timestamp of the last seen event for the specified email analytics events.
|
|
76
|
+
* @param {EmailAnalyticsJobName} jobName - The name of the job to update.
|
|
77
|
+
* @param {'completed'|'started'} field - The field to update.
|
|
78
|
+
* @param {Date} date - The timestamp of the last seen event.
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
* @description
|
|
81
|
+
* Updates the `finished_at` or `started_at` column of the specified job in the `jobs` table with the provided timestamp.
|
|
82
|
+
* This is used to keep track of the last time the job was run to avoid expensive queries following reboot.
|
|
83
|
+
*/
|
|
84
|
+
async setJobTimestamp(jobName, field, date) {
|
|
85
|
+
// Convert string dates to Date objects for SQLite compatibility
|
|
86
|
+
try {
|
|
87
|
+
debug(`Setting ${field} timestamp for job ${jobName} to ${date}`);
|
|
88
|
+
const updateField = field === 'completed' ? 'finished_at' : 'started_at';
|
|
89
|
+
const status = field === 'completed' ? 'finished' : 'started';
|
|
90
|
+
const result = await db.knex('jobs').update({[updateField]: date, updated_at: new Date(), status: status}).where('name', jobName);
|
|
91
|
+
if (result === 0) {
|
|
92
|
+
await db.knex('jobs').insert({
|
|
93
|
+
id: new ObjectID().toHexString(),
|
|
94
|
+
name: jobName,
|
|
95
|
+
[updateField]: date,
|
|
96
|
+
updated_at: date,
|
|
97
|
+
status: status
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
debug(`Error setting ${field} timestamp for job ${jobName}: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sets the status of the specified email analytics job.
|
|
107
|
+
* @param {EmailAnalyticsJobName} jobName - The name of the job to update.
|
|
108
|
+
* @param {'started'|'finished'|'failed'} status - The new status of the job.
|
|
109
|
+
* @returns {Promise<void>}
|
|
110
|
+
* @description
|
|
111
|
+
* Updates the `status` column of the specified job in the `jobs` table with the provided status.
|
|
112
|
+
* This is used to keep track of the current state of the job.
|
|
113
|
+
*/
|
|
114
|
+
async setJobStatus(jobName, status) {
|
|
115
|
+
debug(`Setting status for job ${jobName} to ${status}`);
|
|
116
|
+
try {
|
|
117
|
+
const result = await db.knex('jobs')
|
|
118
|
+
.update({
|
|
119
|
+
status: status,
|
|
120
|
+
updated_at: new Date()
|
|
121
|
+
})
|
|
122
|
+
.where('name', jobName);
|
|
123
|
+
|
|
124
|
+
if (result === 0) {
|
|
125
|
+
await db.knex('jobs').insert({
|
|
126
|
+
id: new ObjectID().toHexString(),
|
|
127
|
+
name: jobName,
|
|
128
|
+
status: status,
|
|
129
|
+
created_at: new Date(),
|
|
130
|
+
updated_at: new Date()
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
debug(`Error setting status for job ${jobName}: ${err.message}`);
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
43
139
|
async aggregateEmailStats(emailId) {
|
|
44
140
|
const {totalCount} = await db.knex('emails').select(db.knex.raw('email_count as totalCount')).where('id', emailId).first() || {totalCount: 0};
|
|
45
141
|
// use IS NULL here because that will typically match far fewer rows than IS NOT NULL making the query faster
|
|
@@ -78,4 +174,4 @@ module.exports = {
|
|
|
78
174
|
.update(updateQuery)
|
|
79
175
|
.where('id', memberId);
|
|
80
176
|
}
|
|
81
|
-
};
|
|
177
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class RequestIntegrityTokenProvider {
|
|
4
|
+
#themeSecret;
|
|
5
|
+
#tokenDuration;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* @param {string} options.themeSecret
|
|
10
|
+
* @param {number} options.tokenDuration - in milliseconds
|
|
11
|
+
*/
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.#themeSecret = options.themeSecret;
|
|
14
|
+
this.#tokenDuration = options.tokenDuration;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
create() {
|
|
21
|
+
const currentTime = Date.now();
|
|
22
|
+
const expiryTime = currentTime + this.#tokenDuration;
|
|
23
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
24
|
+
const hmac = crypto.createHmac('sha256', this.#themeSecret);
|
|
25
|
+
hmac.update(`${expiryTime.toString()}:${nonce}`);
|
|
26
|
+
return `${expiryTime.toString()}:${nonce}:${hmac.digest('hex')}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} token
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
validate(token) {
|
|
34
|
+
const parts = token.split(':');
|
|
35
|
+
if (parts.length !== 3) {
|
|
36
|
+
// Invalid token string
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const timestamp = parseInt(parts[0], 10);
|
|
41
|
+
const nonce = parts[1];
|
|
42
|
+
const hmacDigest = parts[2];
|
|
43
|
+
|
|
44
|
+
const hmac = crypto.createHmac('sha256', this.#themeSecret);
|
|
45
|
+
hmac.update(`${timestamp.toString()}:${nonce}`);
|
|
46
|
+
const expectedHmac = hmac.digest('hex');
|
|
47
|
+
|
|
48
|
+
if (expectedHmac !== hmacDigest) {
|
|
49
|
+
// HMAC mismatch
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Date.now() > timestamp) {
|
|
54
|
+
// Token expired
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = RequestIntegrityTokenProvider;
|
|
@@ -35,7 +35,7 @@ const getFreeTier = async function getFreeTier() {
|
|
|
35
35
|
* @param {import('express').Request} req - The member object
|
|
36
36
|
* @param {import('express').Response} res - The express response object to set the cookies on
|
|
37
37
|
* @param {Object} freeTier - The free tier object
|
|
38
|
-
* @returns
|
|
38
|
+
* @returns
|
|
39
39
|
*/
|
|
40
40
|
const setAccessCookies = function setAccessCookies(member, req, res, freeTier) {
|
|
41
41
|
if (!member) {
|
|
@@ -157,6 +157,44 @@ const getIdentityToken = async function getIdentityToken(req, res) {
|
|
|
157
157
|
}
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
+
const createIntegrityToken = async function createIntegrityToken(req, res) {
|
|
161
|
+
try {
|
|
162
|
+
const token = membersService.requestIntegrityTokenProvider.create();
|
|
163
|
+
res.writeHead(200);
|
|
164
|
+
res.end(token);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
res.writeHead(204);
|
|
167
|
+
res.end();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const verifyIntegrityToken = async function verifyIntegrityToken(req, res, next) {
|
|
172
|
+
const shouldThrowForInvalidToken = config.get('verifyRequestIntegrity');
|
|
173
|
+
try {
|
|
174
|
+
const token = req.body.integrityToken;
|
|
175
|
+
if (!token) {
|
|
176
|
+
logging.warn('Request with missing integrity token.');
|
|
177
|
+
if (shouldThrowForInvalidToken) {
|
|
178
|
+
throw new errors.BadRequestError();
|
|
179
|
+
} else {
|
|
180
|
+
return next();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (membersService.requestIntegrityTokenProvider.validate(token)) {
|
|
184
|
+
return next();
|
|
185
|
+
} else {
|
|
186
|
+
logging.warn('Request with invalid integrity token.');
|
|
187
|
+
if (shouldThrowForInvalidToken) {
|
|
188
|
+
throw new errors.BadRequestError();
|
|
189
|
+
} else {
|
|
190
|
+
return next();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
next(err);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
160
198
|
const deleteSession = async function deleteSession(req, res) {
|
|
161
199
|
try {
|
|
162
200
|
await membersService.ssr.deleteSession(req, res);
|
|
@@ -397,5 +435,7 @@ module.exports = {
|
|
|
397
435
|
updateMemberNewsletters,
|
|
398
436
|
deleteSession,
|
|
399
437
|
accessInfoSession,
|
|
400
|
-
deleteSuppression
|
|
438
|
+
deleteSuppression,
|
|
439
|
+
createIntegrityToken,
|
|
440
|
+
verifyIntegrityToken
|
|
401
441
|
};
|
|
@@ -19,6 +19,7 @@ const tiersService = require('../tiers');
|
|
|
19
19
|
const VerificationTrigger = require('@tryghost/verification-trigger');
|
|
20
20
|
const DatabaseInfo = require('@tryghost/database-info');
|
|
21
21
|
const settingsHelpers = require('../settings-helpers');
|
|
22
|
+
const RequestIntegrityTokenProvider = require('./RequestIntegrityTokenProvider');
|
|
22
23
|
|
|
23
24
|
const messages = {
|
|
24
25
|
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
|
|
@@ -193,6 +194,11 @@ module.exports = {
|
|
|
193
194
|
ssr: null,
|
|
194
195
|
verificationTrigger: null,
|
|
195
196
|
|
|
197
|
+
requestIntegrityTokenProvider: new RequestIntegrityTokenProvider({
|
|
198
|
+
themeSecret: settingsCache.get('theme_session_secret'),
|
|
199
|
+
tokenDuration: 1000 * 60 * 5
|
|
200
|
+
}),
|
|
201
|
+
|
|
196
202
|
stripeConnect: require('./stripe-connect'),
|
|
197
203
|
|
|
198
204
|
processImport: null,
|