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.
Files changed (201) hide show
  1. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  2. package/components/{tryghost-adapter-manager-5.114.1.tgz → tryghost-adapter-manager-5.115.1.tgz} +0 -0
  3. package/components/{tryghost-announcement-bar-settings-5.114.1.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
  4. package/components/{tryghost-api-framework-5.114.1.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
  5. package/components/tryghost-constants-5.115.1.tgz +0 -0
  6. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.114.1.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
  8. package/components/{tryghost-data-generator-5.114.1.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
  9. package/components/{tryghost-domain-events-5.114.1.tgz → tryghost-domain-events-5.115.1.tgz} +0 -0
  10. package/components/tryghost-donations-5.115.1.tgz +0 -0
  11. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  12. package/components/{tryghost-email-content-generator-5.114.1.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
  13. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  14. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  15. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  16. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  17. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  18. package/components/{tryghost-html-to-plaintext-5.114.1.tgz → tryghost-html-to-plaintext-5.115.1.tgz} +0 -0
  19. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  20. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  21. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  22. package/components/{tryghost-job-manager-5.114.1.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
  23. package/components/{tryghost-link-redirects-5.114.1.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
  24. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  25. package/components/{tryghost-magic-link-5.114.1.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
  26. package/components/{tryghost-mailgun-client-5.114.1.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
  27. package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
  28. package/components/{tryghost-member-events-5.114.1.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
  29. package/components/{tryghost-members-api-5.114.1.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
  30. package/components/{tryghost-members-csv-5.114.1.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
  31. package/components/{tryghost-members-offers-5.114.1.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
  32. package/components/{tryghost-members-payments-5.114.1.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
  33. package/components/{tryghost-milestones-5.114.1.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
  34. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  35. package/components/{tryghost-mw-error-handler-5.114.1.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
  36. package/components/{tryghost-mw-version-match-5.114.1.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
  37. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  38. package/components/{tryghost-post-events-5.114.1.tgz → tryghost-post-events-5.115.1.tgz} +0 -0
  39. package/components/{tryghost-post-revisions-5.114.1.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
  40. package/components/{tryghost-posts-service-5.114.1.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
  41. package/components/{tryghost-prometheus-metrics-5.114.1.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
  42. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  43. package/components/{tryghost-security-5.114.1.tgz → tryghost-security-5.115.1.tgz} +0 -0
  44. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  45. package/components/{tryghost-tiers-5.114.1.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
  46. package/components/{tryghost-webmentions-5.114.1.tgz → tryghost-webmentions-5.115.1.tgz} +0 -0
  47. package/content/themes/casper/LICENSE +1 -1
  48. package/content/themes/casper/README.md +1 -1
  49. package/content/themes/source/LICENSE +1 -1
  50. package/content/themes/source/README.md +1 -1
  51. package/content/themes/source/assets/built/screen.css +1 -1
  52. package/content/themes/source/assets/built/screen.css.map +1 -1
  53. package/content/themes/source/assets/css/screen.css +11 -6
  54. package/content/themes/source/partials/feature-image.hbs +2 -2
  55. package/core/boot.js +3 -1
  56. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
  57. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  58. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
  59. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
  60. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
  61. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  62. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
  63. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
  64. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
  65. package/core/built/admin/assets/{chunk.524.85c5b32bd46b91c147b9.js → chunk.524.2439684964c164c598ab.js} +7 -7
  66. package/core/built/admin/assets/{chunk.582.449a129a8005f03574bd.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
  67. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
  68. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
  69. package/core/built/admin/assets/{ghost-c563138cc2c0767bf6eefc9a2587eaa4.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +182 -160
  70. package/core/built/admin/assets/stats/stats.js +11824 -0
  71. package/core/built/admin/index.html +4 -4
  72. package/core/frontend/helpers/ghost_head.js +3 -1
  73. package/core/frontend/src/cards/css/cta.css +1 -1
  74. package/core/server/api/endpoints/slugs.js +6 -2
  75. package/core/server/data/importer/import-manager.js +2 -2
  76. package/core/server/data/importer/importers/importer-revue.js +128 -0
  77. package/core/server/data/importer/importers/json-to-html.js +107 -0
  78. package/core/server/data/migrations/utils/tables.js +2 -4
  79. package/core/server/data/migrations/versions/5.115/2025-03-24-07-19-27-add-identity-read-permission-to-administrators.js +6 -0
  80. package/core/server/data/schema/fixtures/fixtures.json +2 -1
  81. package/core/server/lib/bootstrap-socket.js +87 -0
  82. package/core/server/lib/package-json/index.js +1 -0
  83. package/core/server/lib/package-json/package-json.js +160 -0
  84. package/core/server/lib/package-json/parse.js +57 -0
  85. package/core/server/models/base/plugins/actions.js +44 -31
  86. package/core/server/models/base/plugins/generate-slug.js +6 -0
  87. package/core/server/notify.js +1 -1
  88. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  89. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  90. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  91. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  92. package/core/server/services/api-version-compatibility/index.js +2 -2
  93. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  94. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  95. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  96. package/core/server/services/audience-feedback/Feedback.js +35 -0
  97. package/core/server/services/audience-feedback/index.js +4 -2
  98. package/core/server/services/auth/session/emails/signin.js +168 -0
  99. package/core/server/services/auth/session/index.js +2 -2
  100. package/core/server/services/auth/session/session-from-token.js +69 -0
  101. package/core/server/services/auth/session/session-service.js +364 -0
  102. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  103. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  104. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  105. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  106. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  107. package/core/server/services/explore-ping/index.js +31 -0
  108. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  109. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  110. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  111. package/core/server/services/invitations/accept.js +5 -2
  112. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  113. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  114. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  115. package/core/server/services/mail-events/MailEvent.js +20 -0
  116. package/core/server/services/mail-events/MailEvent.ts +10 -0
  117. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  118. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  119. package/core/server/services/mail-events/MailEventService.js +124 -0
  120. package/core/server/services/mail-events/MailEventService.ts +169 -0
  121. package/core/server/services/mail-events/index.js +1 -1
  122. package/core/server/services/mail-events/libraries.d.ts +2 -0
  123. package/core/server/services/members/CaptchaService.js +80 -0
  124. package/core/server/services/members/api.js +1 -1
  125. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  126. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  127. package/core/server/services/members/importer/email-template.js +182 -0
  128. package/core/server/services/members/importer/index.js +30 -0
  129. package/core/server/services/members/members-ssr.js +333 -0
  130. package/core/server/services/members/service.js +2 -2
  131. package/core/server/services/posts/stats/PostStats.js +13 -0
  132. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  133. package/core/server/services/route-settings/index.js +1 -1
  134. package/core/server/services/stripe/README.md +63 -0
  135. package/core/server/services/stripe/StripeAPI.js +931 -0
  136. package/core/server/services/stripe/StripeMigrations.js +613 -0
  137. package/core/server/services/stripe/StripeService.js +175 -0
  138. package/core/server/services/stripe/WebhookController.js +100 -0
  139. package/core/server/services/stripe/WebhookManager.js +175 -0
  140. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  141. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  142. package/core/server/services/stripe/events/index.js +4 -0
  143. package/core/server/services/stripe/service.js +1 -1
  144. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  145. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  146. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  147. package/core/server/services/themes/loader.js +1 -1
  148. package/core/server/services/themes/to-json.js +1 -1
  149. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  150. package/core/server/web/shared/middleware/cache-control.js +51 -0
  151. package/core/server/web/shared/middleware/index.js +1 -1
  152. package/core/server/web/well-known.js +1 -1
  153. package/core/shared/labs.js +3 -1
  154. package/core/shared/settings-cache/CacheManager.js +64 -6
  155. package/package.json +103 -134
  156. package/tsconfig.tsbuildinfo +1 -1
  157. package/yarn.lock +7 -93
  158. package/components/tryghost-adapter-cache-redis-5.114.1.tgz +0 -0
  159. package/components/tryghost-api-version-compatibility-service-5.114.1.tgz +0 -0
  160. package/components/tryghost-audience-feedback-5.114.1.tgz +0 -0
  161. package/components/tryghost-bookshelf-repository-5.114.1.tgz +0 -0
  162. package/components/tryghost-bootstrap-socket-5.114.1.tgz +0 -0
  163. package/components/tryghost-captcha-service-5.114.1.tgz +0 -0
  164. package/components/tryghost-constants-5.114.1.tgz +0 -0
  165. package/components/tryghost-custom-fonts-5.114.1.tgz +0 -0
  166. package/components/tryghost-donations-5.114.1.tgz +0 -0
  167. package/components/tryghost-email-addresses-5.114.1.tgz +0 -0
  168. package/components/tryghost-email-analytics-provider-mailgun-5.114.1.tgz +0 -0
  169. package/components/tryghost-email-analytics-service-5.114.1.tgz +0 -0
  170. package/components/tryghost-email-events-5.114.1.tgz +0 -0
  171. package/components/tryghost-email-service-5.114.1.tgz +0 -0
  172. package/components/tryghost-email-suppression-list-5.114.1.tgz +0 -0
  173. package/components/tryghost-express-dynamic-redirects-5.114.1.tgz +0 -0
  174. package/components/tryghost-extract-api-key-5.114.1.tgz +0 -0
  175. package/components/tryghost-ghost-5.114.1.tgz +0 -0
  176. package/components/tryghost-i18n-5.114.1.tgz +0 -0
  177. package/components/tryghost-identity-token-service-5.114.1.tgz +0 -0
  178. package/components/tryghost-importer-handler-content-files-5.114.1.tgz +0 -0
  179. package/components/tryghost-importer-revue-5.114.1.tgz +0 -0
  180. package/components/tryghost-in-memory-repository-5.114.1.tgz +0 -0
  181. package/components/tryghost-link-replacer-5.114.1.tgz +0 -0
  182. package/components/tryghost-mail-events-5.114.1.tgz +0 -0
  183. package/components/tryghost-member-attribution-5.114.1.tgz +0 -0
  184. package/components/tryghost-members-importer-5.114.1.tgz +0 -0
  185. package/components/tryghost-members-ssr-5.114.1.tgz +0 -0
  186. package/components/tryghost-members-stripe-service-5.114.1.tgz +0 -0
  187. package/components/tryghost-minifier-5.114.1.tgz +0 -0
  188. package/components/tryghost-mw-api-version-mismatch-5.114.1.tgz +0 -0
  189. package/components/tryghost-mw-cache-control-5.114.1.tgz +0 -0
  190. package/components/tryghost-mw-session-from-token-5.114.1.tgz +0 -0
  191. package/components/tryghost-mw-update-user-last-seen-5.114.1.tgz +0 -0
  192. package/components/tryghost-mw-vhost-5.114.1.tgz +0 -0
  193. package/components/tryghost-package-json-5.114.1.tgz +0 -0
  194. package/components/tryghost-recommendations-5.114.1.tgz +0 -0
  195. package/components/tryghost-referrers-5.114.1.tgz +0 -0
  196. package/components/tryghost-session-service-5.114.1.tgz +0 -0
  197. package/components/tryghost-settings-path-manager-5.114.1.tgz +0 -0
  198. package/components/tryghost-slack-notifications-5.114.1.tgz +0 -0
  199. package/components/tryghost-version-notifications-data-service-5.114.1.tgz +0 -0
  200. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  201. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
@@ -1,5 +1,9 @@
1
1
  const urlUtils = require('../../../shared/url-utils');
2
2
  const urlService = require('../../services/url');
3
+
4
+ const AudienceFeedbackService = require('./AudienceFeedbackService');
5
+ const AudienceFeedbackController = require('./AudienceFeedbackController');
6
+ const Feedback = require('./Feedback');
3
7
  const FeedbackRepository = require('./FeedbackRepository');
4
8
 
5
9
  class AudienceFeedbackServiceWrapper {
@@ -12,8 +16,6 @@ class AudienceFeedbackServiceWrapper {
12
16
  // Wire up all the dependencies
13
17
  const models = require('../../models');
14
18
 
15
- const {AudienceFeedbackService, AudienceFeedbackController, Feedback} = require('@tryghost/audience-feedback');
16
-
17
19
  this.repository = new FeedbackRepository({
18
20
  Member: models.Member,
19
21
  MemberFeedback: models.MemberFeedback,
@@ -0,0 +1,168 @@
1
+ module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, deviceDetails, is2FARequired}) => `
2
+ <!doctype html>
3
+ <html>
4
+ <head>
5
+ <meta name="viewport" content="width=device-width">
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
+ <title>🔑 ${t('Your verification code for {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</title>
8
+ <style>
9
+ /* -------------------------------------
10
+ RESPONSIVE AND MOBILE FRIENDLY STYLES
11
+ ------------------------------------- */
12
+ @media only screen and (max-width: 620px) {
13
+ table[class=body] h1 {
14
+ font-size: 28px !important;
15
+ margin-bottom: 10px !important;
16
+ }
17
+ table[class=body] p,
18
+ table[class=body] ul,
19
+ table[class=body] ol,
20
+ table[class=body] td,
21
+ table[class=body] span,
22
+ table[class=body] a {
23
+ font-size: 16px !important;
24
+ }
25
+ table[class=body] .wrapper,
26
+ table[class=body] .article {
27
+ padding: 10px !important;
28
+ }
29
+ table[class=body] .content {
30
+ padding: 0 !important;
31
+ }
32
+ table[class=body] .container {
33
+ padding: 0 !important;
34
+ width: 100% !important;
35
+ }
36
+ table[class=body] .main {
37
+ border-left-width: 0 !important;
38
+ border-radius: 0 !important;
39
+ border-right-width: 0 !important;
40
+ }
41
+ table[class=body] .btn table {
42
+ width: 100% !important;
43
+ }
44
+ table[class=body] .btn a {
45
+ width: 100% !important;
46
+ }
47
+ table[class=body] .img-responsive {
48
+ height: auto !important;
49
+ max-width: 100% !important;
50
+ width: auto !important;
51
+ }
52
+ table[class=body] p[class=small],
53
+ table[class=body] a[class=small] {
54
+ font-size: 11px !important;
55
+ }
56
+ }
57
+ /* -------------------------------------
58
+ PRESERVE THESE STYLES IN THE HEAD
59
+ ------------------------------------- */
60
+ @media all {
61
+ .ExternalClass {
62
+ width: 100%;
63
+ }
64
+ .ExternalClass,
65
+ .ExternalClass p,
66
+ .ExternalClass span,
67
+ .ExternalClass font,
68
+ .ExternalClass td,
69
+ .ExternalClass div {
70
+ line-height: 100%;
71
+ }
72
+ .recipient-link a {
73
+ color: inherit !important;
74
+ font-family: inherit !important;
75
+ font-size: inherit !important;
76
+ font-weight: inherit !important;
77
+ line-height: inherit !important;
78
+ text-decoration: none !important;
79
+ }
80
+ #MessageViewBody a {
81
+ color: inherit;
82
+ text-decoration: none;
83
+ font-size: inherit;
84
+ font-family: inherit;
85
+ font-weight: inherit;
86
+ line-height: inherit;
87
+ }
88
+ }
89
+ hr {
90
+ border-width: 0;
91
+ height: 0;
92
+ margin-top: 34px;
93
+ margin-bottom: 34px;
94
+ border-bottom-width: 1px;
95
+ border-bottom-color: #EEF5F8;
96
+ }
97
+ a {
98
+ color: #3A464C;
99
+ }
100
+ </style>
101
+ </head>
102
+ <body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
103
+ <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
104
+ <tr>
105
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
106
+ <td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
107
+ <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
108
+
109
+ <!-- START CENTERED CONTAINER -->
110
+ <span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${t('Here\'s your code to login to {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</span>
111
+ <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
112
+
113
+ <!-- START MAIN CONTENT AREA -->
114
+ <tr>
115
+ <td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
116
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
117
+ <tr>
118
+ <td align="center" style="padding-top: 20px; padding-bottom: 12px;"><img src="${siteLogo}" width="60" height="60" style="width: 60px; height: 60px;" /></td>
119
+ </tr>
120
+ <tr>
121
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
122
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: 600; line-height: 24px; margin: 0; margin-bottom: 15px; margin-top: 50px;">${t('Sign in verification')}</p>
123
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 24px; margin-bottom: 32px;">${is2FARequired ? '' : t('You just tried to access your account from a new device.')} ${t('For security verification, enter the code below to sign in to {{siteTitle}}:', {siteTitle, interpolation: {escapeValue: false}})}</p>
124
+ </td>
125
+ </tr>
126
+ <tr>
127
+ <td style="padding: 16px; background-color: #F4F5F6; border-radius: 8px; text-align: center; vertical-align: middle;" valign="middle">
128
+ <h2 style="text-align: center; vertical-align: center; letter-spacing: 5px; font-size: 24px; color: #15212A; font-weight: 600; line-height: 24px; margin: 0;">${token}</h2>
129
+ </td>
130
+ </tr>
131
+ <tr>
132
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
133
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 4px; margin-top: 24px;">${t('Device:')} <strong style="font-weight: 600;">${deviceDetails.device}</strong></p>
134
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 4px; margin-top: 0px;">${t('Where:')} <strong style="font-weight: 600;">${deviceDetails.location}</strong></p>
135
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 4px; margin-top: 0px;">${t('When:')} <strong style="font-weight: 600;">${deviceDetails.time}</strong></p>
136
+ </td>
137
+ </tr>
138
+ <tr>
139
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
140
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 11px; margin-top: 24px;">${t('If you didn\'t try to sign in recently, you can safely ignore this email to deny access.')}</p>
141
+ </td>
142
+ </tr>
143
+
144
+ <!-- START FOOTER -->
145
+ <tr>
146
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
147
+ <p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 16px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0;">This message was sent from <a class="small" href="${siteUrl}" style="text-decoration: underline; color: #738A94; font-size: 11px;">${siteDomain}</a> to <a class="small" href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 11px;">${email}</a></p>
148
+ </td>
149
+ </tr>
150
+
151
+ <!-- END FOOTER -->
152
+ </table>
153
+ </td>
154
+ </tr>
155
+
156
+ <!-- END MAIN CONTENT AREA -->
157
+ </table>
158
+
159
+
160
+ <!-- END CENTERED CONTAINER -->
161
+ </div>
162
+ </td>
163
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
164
+ </tr>
165
+ </table>
166
+ </body>
167
+ </html>
168
+ `;
@@ -1,6 +1,6 @@
1
1
  const adapterManager = require('../../adapter-manager');
2
- const createSessionService = require('@tryghost/session-service');
3
- const sessionFromToken = require('@tryghost/mw-session-from-token');
2
+ const createSessionService = require('./session-service');
3
+ const sessionFromToken = require('./session-from-token');
4
4
  const createSessionMiddleware = require('./middleware');
5
5
  const settingsCache = require('../../../../shared/settings-cache');
6
6
  const {GhostMailer} = require('../../mail');
@@ -0,0 +1,69 @@
1
+ module.exports = SessionFromToken;
2
+
3
+ /**
4
+ * @typedef {object} User
5
+ * @prop {string} id
6
+ */
7
+
8
+ /**
9
+ * @typedef {import('express').Request} Req
10
+ * @typedef {import('express').Response} Res
11
+ * @typedef {import('express').NextFunction} Next
12
+ * @typedef {import('express').RequestHandler} RequestHandler
13
+ */
14
+
15
+ /**
16
+ * Returns a connect middleware function which exchanges a token for a session
17
+ *
18
+ * @template Token
19
+ * @template Lookup
20
+ *
21
+ * @param { object } deps
22
+ * @param { (req: Req) => Promise<Token> } deps.getTokenFromRequest
23
+ * @param { (token: Token) => Promise<Lookup> } deps.getLookupFromToken
24
+ * @param { (lookup: Lookup) => Promise<User> } deps.findUserByLookup
25
+ * @param { (req: Req, res: Res, user: User) => Promise<void> } deps.createSession
26
+ * @param { boolean } deps.callNextWithError - Whether next should be call with an error or just pass through
27
+ *
28
+ * @returns {RequestHandler}
29
+ */
30
+ function SessionFromToken({
31
+ getTokenFromRequest,
32
+ getLookupFromToken,
33
+ findUserByLookup,
34
+ createSession,
35
+ callNextWithError
36
+ }) {
37
+ /**
38
+ * @param {Req} req
39
+ * @param {Res} res
40
+ * @param {Next} next
41
+ * @returns {Promise<void>}
42
+ */
43
+ async function handler(req, res, next) {
44
+ try {
45
+ const token = await getTokenFromRequest(req);
46
+ if (!token) {
47
+ return next();
48
+ }
49
+ const email = await getLookupFromToken(token);
50
+ if (!email) {
51
+ return next();
52
+ }
53
+ const user = await findUserByLookup(email);
54
+ if (!user) {
55
+ return next();
56
+ }
57
+ await createSession(req, res, user);
58
+ next();
59
+ } catch (err) {
60
+ if (callNextWithError) {
61
+ next(err);
62
+ } else {
63
+ next();
64
+ }
65
+ }
66
+ }
67
+
68
+ return handler;
69
+ }
@@ -0,0 +1,364 @@
1
+ const {
2
+ BadRequestError
3
+ } = require('@tryghost/errors');
4
+ const errors = require('@tryghost/errors');
5
+ const emailTemplate = require('./emails/signin');
6
+ const UAParser = require('ua-parser-js');
7
+ const got = require('got');
8
+ const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
9
+ const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
10
+
11
+ const {totp} = require('otplib');
12
+ totp.options = {
13
+ digits: 6,
14
+ step: 60,
15
+ window: [10, 10]
16
+ };
17
+
18
+ /**
19
+ * @typedef {object} User
20
+ * @prop {string} id
21
+ * @prop {(attr: string) => string} get
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} Session
26
+ * @prop {(cb: (err: Error | null) => any) => void} destroy
27
+ * @prop {string} user_id
28
+ * @prop {string} origin
29
+ * @prop {string} user_agent
30
+ * @prop {string} ip
31
+ * @prop {boolean} verified
32
+ */
33
+
34
+ /**
35
+ * @typedef {import('express').Request} Req
36
+ * @typedef {import('express').Response} Res
37
+ */
38
+
39
+ /**
40
+ * @typedef {object} SessionService
41
+ * @prop {(req: Req, res: Res) => Promise<User | null>} getUserForSession
42
+ * @prop {(req: Req, res: Res) => Promise<void>} removeUserForSession
43
+ * @prop {(req: Req, res: Res, user: User) => Promise<void>} createSessionForUser
44
+ * @prop {(req: Req, res: Res) => Promise<void>} createVerifiedSessionForUser
45
+ * @prop {(req: Req, res: Res) => Promise<void>} verifySession
46
+ * @prop {(req: Req, res: Res) => Promise<void>} sendAuthCodeToUser
47
+ * @prop {(req: Req, res: Res) => Promise<boolean>} verifyAuthCodeForUser
48
+ * @prop {(req: Req, res: Res) => Promise<boolean>} isVerifiedSession
49
+ */
50
+
51
+ /**
52
+ * @param {object} deps
53
+ * @param {(req: Req, res: Res) => Promise<Session>} deps.getSession
54
+ * @param {(data: {id: string}) => Promise<User>} deps.findUserById
55
+ * @param {(req: Req) => string} deps.getOriginOfRequest
56
+ * @param {(key: string) => string} deps.getSettingsCache
57
+ * @param {() => string} deps.getBlogLogo
58
+ * @param {import('../../core/core/server/services/mail').GhostMailer} deps.mailer
59
+ * @param {import('../../core/core/shared/labs')} deps.labs
60
+ * @param {import('../../core/core/server/services/i18n').t} deps.t
61
+ * @param {import('../../core/core/shared/url-utils')} deps.urlUtils
62
+ * @returns {SessionService}
63
+ */
64
+
65
+ module.exports = function createSessionService({
66
+ getSession,
67
+ findUserById,
68
+ getOriginOfRequest,
69
+ getSettingsCache,
70
+ getBlogLogo,
71
+ mailer,
72
+ urlUtils,
73
+ labs,
74
+ t
75
+ }) {
76
+ /**
77
+ * cookieCsrfProtection
78
+ *
79
+ * @param {Req} req
80
+ * @param {Session} session
81
+ * @returns {Promise<void>}
82
+ */
83
+ function cookieCsrfProtection(req, session) {
84
+ // If there is no origin on the session object it means this is a *new*
85
+ // session, that hasn't been initialised yet. So we don't need CSRF protection
86
+ if (!session.origin) {
87
+ return;
88
+ }
89
+
90
+ const origin = getOriginOfRequest(req);
91
+
92
+ if (session.origin !== origin) {
93
+ throw new BadRequestError({
94
+ message: `Request made from incorrect origin. Expected '${session.origin}' received '${origin}'.`
95
+ });
96
+ }
97
+ }
98
+
99
+ /**
100
+ * createSessionForUser
101
+ *
102
+ * @param {Req} req
103
+ * @param {Res} res
104
+ * @param {User} user
105
+ * @returns {Promise<void>}
106
+ */
107
+ async function createSessionForUser(req, res, user) {
108
+ const session = await getSession(req, res);
109
+ const origin = getOriginOfRequest(req);
110
+ if (!origin) {
111
+ throw new BadRequestError({
112
+ message: 'Could not determine origin of request. Please ensure an Origin or Referrer header is present.'
113
+ });
114
+ }
115
+
116
+ session.user_id = user.id;
117
+ session.origin = origin;
118
+ session.user_agent = req.get('user-agent');
119
+ session.ip = req.ip;
120
+
121
+ if (!labs.isSet('staff2fa')) {
122
+ session.verified = true;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * createVerifiedSessionForUser
128
+ *
129
+ * @param {Req} req
130
+ * @param {Res} res
131
+ * @param {User} user
132
+ * @returns {Promise<void>}
133
+ */
134
+ async function createVerifiedSessionForUser(req, res, user) {
135
+ await createSessionForUser(req, res, user);
136
+ await verifySession(req, res);
137
+ }
138
+
139
+ /**
140
+ * generateAuthCodeForUser
141
+ *
142
+ * @param {Req} req
143
+ * @param {Res} res
144
+ * @returns {Promise<string>}
145
+ */
146
+ async function generateAuthCodeForUser(req, res) {
147
+ const session = await getSession(req, res);
148
+ const secret = getSettingsCache('admin_session_secret') + session.user_id;
149
+ const token = totp.generate(secret);
150
+ return token;
151
+ }
152
+
153
+ /**
154
+ * verifyAuthCodeForUser
155
+ *
156
+ * @param {Req} req
157
+ * @param {Res} res
158
+ * @returns {Promise<boolean>}
159
+ */
160
+ async function verifyAuthCodeForUser(req, res) {
161
+ const session = await getSession(req, res);
162
+ const secret = getSettingsCache('admin_session_secret') + session.user_id;
163
+ const isValid = totp.check(req.body.token, secret);
164
+ return isValid;
165
+ }
166
+
167
+ const formatTime = new Intl.DateTimeFormat('en-GB', {
168
+ day: '2-digit',
169
+ month: 'short',
170
+ year: 'numeric',
171
+ hour: '2-digit',
172
+ minute: '2-digit',
173
+ timeZone: 'UTC',
174
+ timeZoneName: 'short'
175
+ }).format;
176
+
177
+ /**
178
+ * Get a readable location string from an IP address.
179
+ * @param {string} ip - The IP address to look up.
180
+ * @returns {Promise<string>} - A readable location string or 'Unknown'.
181
+ */
182
+ async function getGeolocationFromIP(ip) {
183
+ if (!ip || (!IPV4_REGEX.test(ip) && !IPV6_REGEX.test(ip))) {
184
+ return 'Unknown';
185
+ }
186
+
187
+ const gotOpts = {
188
+ timeout: 500
189
+ };
190
+
191
+ if (process.env.NODE_ENV?.startsWith('test')) {
192
+ gotOpts.retry = 0;
193
+ }
194
+
195
+ const geojsUrl = `https://get.geojs.io/v1/ip/geo/${encodeURIComponent(ip)}.json`;
196
+
197
+ try {
198
+ const response = await got(geojsUrl, gotOpts).json();
199
+
200
+ const {city, region, country} = response || {};
201
+
202
+ // Only include non-empty parts in the result
203
+ const locationParts = [city, region, country].filter(Boolean);
204
+
205
+ // If no valid parts, return 'Unknown'
206
+ return locationParts.length > 0 ? locationParts.join(', ').trim() : 'Unknown';
207
+ } catch (error) {
208
+ return 'Unknown';
209
+ }
210
+ }
211
+
212
+ async function getDeviceDetails(userAgent, ip) {
213
+ const parser = new UAParser();
214
+ parser.setUA(userAgent);
215
+ const result = parser.getResult();
216
+ const deviceParts = [
217
+ result.browser?.name || '',
218
+ result.os?.name || ''
219
+ ].filter(Boolean);
220
+
221
+ return {
222
+ device: deviceParts.join(', '),
223
+ location: await getGeolocationFromIP(ip),
224
+ time: formatTime(new Date())
225
+ };
226
+ }
227
+
228
+ /**
229
+ * sendAuthCodeToUser
230
+ *
231
+ * @param {Req} req
232
+ * @param {Res} res
233
+ * @returns {Promise<void>}
234
+ */
235
+ async function sendAuthCodeToUser(req, res) {
236
+ const session = await getSession(req, res);
237
+ const token = await generateAuthCodeForUser(req, res);
238
+
239
+ let user;
240
+ try {
241
+ user = await findUserById({id: session.user_id});
242
+ } catch (error) {
243
+ // User session likely doesn't contain a valid user ID
244
+ throw new BadRequestError({
245
+ message: 'Could not fetch user from the session.'
246
+ });
247
+ }
248
+
249
+ const recipient = user.get('email');
250
+ const siteTitle = getSettingsCache('title');
251
+ const siteLogo = getBlogLogo();
252
+ const siteUrl = urlUtils.urlFor('home', true);
253
+ const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
254
+ const siteDomain = (domain && domain[1]);
255
+ const email = emailTemplate({
256
+ t,
257
+ siteTitle: siteTitle,
258
+ email: recipient,
259
+ siteDomain: siteDomain,
260
+ siteUrl: siteUrl,
261
+ siteLogo: siteLogo,
262
+ token: token,
263
+ deviceDetails: await getDeviceDetails(session.user_agent, session.ip),
264
+ is2FARequired: getSettingsCache('require_email_mfa')
265
+ });
266
+
267
+ try {
268
+ await mailer.send({
269
+ to: recipient,
270
+ subject: `${token} is your Ghost sign in verification code`,
271
+ html: email
272
+ });
273
+ } catch (error) {
274
+ throw new errors.EmailError({
275
+ ...error,
276
+ message: 'Failed to send email. Please check your site configuration and try again.'
277
+ });
278
+ }
279
+ }
280
+
281
+ /**
282
+ * verifySession
283
+ *
284
+ * @param {Req} req
285
+ * @param {Res} res
286
+ */
287
+ async function verifySession(req, res) {
288
+ const session = await getSession(req, res);
289
+ session.verified = true;
290
+ }
291
+
292
+ /**
293
+ * isVerifiedSession
294
+ *
295
+ * @param {Req} req
296
+ * @param {Res} res
297
+ */
298
+ async function isVerifiedSession(req, res) {
299
+ const session = await getSession(req, res);
300
+ return session.verified;
301
+ }
302
+
303
+ /**
304
+ * removeUserForSession
305
+ *
306
+ * @param {Req} req
307
+ * @param {Res} res
308
+ * @returns {Promise<void>}
309
+ */
310
+ async function removeUserForSession(req, res) {
311
+ const session = await getSession(req, res);
312
+
313
+ const requireMfa = getSettingsCache('require_email_mfa');
314
+ if (requireMfa) {
315
+ session.verified = undefined;
316
+ }
317
+
318
+ session.user_id = undefined;
319
+ }
320
+
321
+ /**
322
+ * getUserForSession
323
+ *
324
+ * @param {Req} req
325
+ * @param {Res} res
326
+ * @returns {Promise<User | null>}
327
+ */
328
+ async function getUserForSession(req, res) {
329
+ // CASE: we don't have a cookie header so allow fallthrough to other
330
+ // auth middleware or final "ensure authenticated" check
331
+ if (!req.headers || !req.headers.cookie) {
332
+ return null;
333
+ }
334
+
335
+ const session = await getSession(req, res);
336
+ // Enable CSRF bypass (useful for OAuth for example)
337
+ if (!res || !res.locals || !res.locals.bypassCsrfProtection) {
338
+ cookieCsrfProtection(req, session);
339
+ }
340
+
341
+ if (!session || !session.user_id) {
342
+ return null;
343
+ }
344
+
345
+ try {
346
+ const user = await findUserById({id: session.user_id});
347
+ return user;
348
+ } catch (err) {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ return {
354
+ getUserForSession,
355
+ createSessionForUser,
356
+ createVerifiedSessionForUser,
357
+ removeUserForSession,
358
+ verifySession,
359
+ isVerifiedSession,
360
+ sendAuthCodeToUser,
361
+ verifyAuthCodeForUser,
362
+ generateAuthCodeForUser
363
+ };
364
+ };