ghost 5.119.2 → 5.120.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.
Files changed (130) hide show
  1. package/components/tryghost-i18n-5.120.0.tgz +0 -0
  2. package/core/boot.js +0 -2
  3. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +7555 -7216
  4. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-60ce658c.mjs → CodeEditorView-1c5b0683.mjs} +2 -2
  5. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  6. package/core/built/admin/assets/admin-x-settings/{index-8480baa8.mjs → index-14e518a7.mjs} +3 -3
  7. package/core/built/admin/assets/admin-x-settings/{index-a2648c61.mjs → index-fc9f985b.mjs} +2 -2
  8. package/core/built/admin/assets/admin-x-settings/{modals-6900c1d5.mjs → modals-15bc6a0f.mjs} +7192 -6656
  9. package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js → chunk.383.25fca2f09b4896656125.js} +76 -59
  10. package/core/built/admin/assets/chunk.524.1657b12c0ab25dd9fb79.js +28 -0
  11. package/core/built/admin/assets/{chunk.582.98a820cbc4bb65f2e685.js → chunk.582.09869b1f1a3cc0ab81f6.js} +19 -26
  12. package/core/built/admin/assets/{ghost-843572e9507d099162ae744d791daba1.js → ghost-b3b44421acca3b3eec76bfbb6ba0e81b.js} +3 -3
  13. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12578 -12352
  14. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +423 -211
  15. package/core/built/admin/assets/posts/posts.js +13680 -13671
  16. package/core/built/admin/assets/stats/stats.js +16457 -16635
  17. package/core/built/admin/assets/{vendor-8f805740fee4db959a5b2119001a56b1.js → vendor-4ce6d282a2a00fe486a0951e0591da19.js} +11 -9
  18. package/core/built/admin/index.html +5 -5
  19. package/core/frontend/helpers/match.js +6 -0
  20. package/core/frontend/services/routing/ParentRouter.js +1 -1
  21. package/core/frontend/services/routing/controllers/email-post.js +0 -2
  22. package/core/frontend/services/routing/controllers/previews.js +0 -3
  23. package/core/frontend/web/middleware/frontend-caching.js +2 -2
  24. package/core/server/api/endpoints/authentication.js +37 -73
  25. package/core/server/api/endpoints/authors-public.js +8 -9
  26. package/core/server/api/endpoints/db.js +34 -35
  27. package/core/server/api/endpoints/emails.js +8 -10
  28. package/core/server/api/endpoints/integrations.js +20 -18
  29. package/core/server/api/endpoints/invites.js +8 -10
  30. package/core/server/api/endpoints/labels.js +19 -23
  31. package/core/server/api/endpoints/notifications.js +3 -4
  32. package/core/server/api/endpoints/pages-public.js +8 -10
  33. package/core/server/api/endpoints/pages.js +14 -18
  34. package/core/server/api/endpoints/posts-public.js +8 -10
  35. package/core/server/api/endpoints/posts.js +6 -8
  36. package/core/server/api/endpoints/previews.js +8 -10
  37. package/core/server/api/endpoints/redirects.js +7 -8
  38. package/core/server/api/endpoints/schedules.js +5 -7
  39. package/core/server/api/endpoints/slugs.js +7 -9
  40. package/core/server/api/endpoints/snippets.js +16 -20
  41. package/core/server/api/endpoints/tags-public.js +8 -10
  42. package/core/server/api/endpoints/tags.js +19 -23
  43. package/core/server/api/endpoints/themes.js +6 -8
  44. package/core/server/api/endpoints/users.js +31 -36
  45. package/core/server/api/endpoints/utils/permissions.js +10 -10
  46. package/core/server/api/endpoints/utils/serializers/output/roles.js +9 -10
  47. package/core/server/api/endpoints/utils/validators/input/images.js +43 -52
  48. package/core/server/api/endpoints/utils/validators/input/invites.js +6 -8
  49. package/core/server/api/endpoints/webhooks.js +38 -42
  50. package/core/server/data/migrations/versions/5.120/2025-05-07-14-57-38-add-newsletters-button-corners-column.js +8 -0
  51. package/core/server/data/migrations/versions/5.120/2025-05-13-17-36-56-add-newsletters-button-style-column.js +8 -0
  52. package/core/server/data/migrations/versions/5.120/2025-05-14-20-00-15-add-newsletters-setting-columns.js +22 -0
  53. package/core/server/data/schema/schema.js +6 -1
  54. package/core/server/lib/image/Gravatar.js +12 -13
  55. package/core/server/lib/lexical.js +3 -1
  56. package/core/server/models/newsletter.js +6 -1
  57. package/core/server/services/api-version-compatibility/index.js +1 -33
  58. package/core/server/services/auth/session/emails/signin.js +3 -3
  59. package/core/server/services/email-address/EmailAddressParser.js +52 -0
  60. package/core/server/services/email-address/EmailAddressParser.js.d.ts +13 -0
  61. package/core/server/services/email-address/EmailAddressService.js +142 -0
  62. package/core/server/services/email-address/EmailAddressService.ts +183 -0
  63. package/core/server/services/email-address/EmailAddressServiceWrapper.js +2 -4
  64. package/core/server/services/email-analytics/EmailAnalyticsService.js +1 -1
  65. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +2 -1
  66. package/core/server/services/email-service/BatchSendingService.js +703 -0
  67. package/core/server/services/email-service/EmailBodyCache.js +20 -0
  68. package/core/server/services/email-service/EmailController.js +94 -0
  69. package/core/server/services/email-service/EmailEventProcessor.js +267 -0
  70. package/core/server/services/email-service/EmailEventStorage.js +187 -0
  71. package/core/server/services/email-service/EmailRenderer.js +1263 -0
  72. package/core/server/services/email-service/EmailSegmenter.js +74 -0
  73. package/core/server/services/email-service/EmailService.js +310 -0
  74. package/core/server/services/email-service/EmailServiceWrapper.js +9 -2
  75. package/core/server/services/email-service/MailgunEmailProvider.js +191 -0
  76. package/core/server/services/email-service/SendingService.js +173 -0
  77. package/core/server/services/email-service/email-templates/partials/feedback-button.hbs +7 -0
  78. package/core/server/services/email-service/email-templates/partials/latest-posts.hbs +39 -0
  79. package/core/server/services/email-service/email-templates/partials/paywall.hbs +20 -0
  80. package/core/server/services/email-service/email-templates/partials/styles.hbs +2348 -0
  81. package/core/server/services/email-service/email-templates/template.hbs +238 -0
  82. package/core/server/services/email-service/events/EmailBouncedEvent.js +63 -0
  83. package/core/server/services/email-service/events/EmailDeliveredEvent.js +49 -0
  84. package/core/server/services/email-service/events/EmailOpenedEvent.js +49 -0
  85. package/core/server/services/email-service/events/EmailTemporaryBouncedEvent.js +63 -0
  86. package/core/server/services/email-service/events/EmailUnsubscribedEvent.js +42 -0
  87. package/core/server/services/email-service/events/SpamComplaintEvent.js +42 -0
  88. package/core/server/services/email-service/helpers/register-helpers.js +59 -0
  89. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +2 -1
  90. package/core/server/services/explore-ping/index.js +2 -1
  91. package/core/server/services/mail/GhostMailer.js +1 -1
  92. package/core/server/services/media-inliner/ExternalMediaInliner.js +2 -1
  93. package/core/server/services/members/api.js +15 -15
  94. package/core/server/services/members/emails/signin.js +4 -4
  95. package/core/server/services/members/emails/signup-paid.js +3 -4
  96. package/core/server/services/members/emails/signup.js +3 -3
  97. package/core/server/services/members/emails/subscribe.js +3 -3
  98. package/core/server/services/members/members-api/controllers/RouterController.js +50 -36
  99. package/core/server/services/members/members-api/repositories/MemberRepository.js +92 -92
  100. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  101. package/core/server/services/settings-helpers/SettingsHelpers.js +1 -1
  102. package/core/server/services/staff/StaffServiceEmails.js +1 -1
  103. package/core/server/services/stats/PostsStatsService.js +28 -7
  104. package/core/server/web/api/app.js +0 -1
  105. package/core/server/web/api/endpoints/admin/app.js +0 -2
  106. package/core/server/web/api/endpoints/content/app.js +0 -2
  107. package/core/server/web/api/middleware/upload.js +2 -2
  108. package/core/shared/custom-theme-settings-cache/CustomThemeSettingsService.js +2 -1
  109. package/package.json +39 -97
  110. package/tsconfig.tsbuildinfo +1 -1
  111. package/yarn.lock +385 -517
  112. package/components/tryghost-api-framework-5.119.2.tgz +0 -0
  113. package/components/tryghost-custom-fonts-5.119.2.tgz +0 -0
  114. package/components/tryghost-domain-events-5.119.2.tgz +0 -0
  115. package/components/tryghost-email-addresses-5.119.2.tgz +0 -0
  116. package/components/tryghost-email-service-5.119.2.tgz +0 -0
  117. package/components/tryghost-html-to-plaintext-5.119.2.tgz +0 -0
  118. package/components/tryghost-i18n-5.119.2.tgz +0 -0
  119. package/components/tryghost-job-manager-5.119.2.tgz +0 -0
  120. package/components/tryghost-members-csv-5.119.2.tgz +0 -0
  121. package/components/tryghost-mw-error-handler-5.119.2.tgz +0 -0
  122. package/components/tryghost-mw-vhost-5.119.2.tgz +0 -0
  123. package/components/tryghost-prometheus-metrics-5.119.2.tgz +0 -0
  124. package/components/tryghost-security-5.119.2.tgz +0 -0
  125. package/core/built/admin/assets/chunk.524.b8545af3bb714bc4f820.js +0 -35
  126. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +0 -99
  127. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +0 -80
  128. package/core/server/services/api-version-compatibility/extract-api-key.js +0 -57
  129. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +0 -31
  130. /package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js.LICENSE.txt → chunk.383.25fca2f09b4896656125.js.LICENSE.txt} +0 -0
@@ -79,16 +79,16 @@ function createApiInstance(config) {
79
79
  const siteTitle = settingsCache.get('title');
80
80
  switch (type) {
81
81
  case 'subscribe':
82
- return `📫 ${t(`Confirm your subscription to {{siteTitle}}`, {siteTitle, interpolation: {escapeValue: false}})}`;
82
+ return `📫 ${t(`Confirm your subscription to {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`;
83
83
  case 'signup':
84
- return `🙌 ${t(`Complete your sign up to {{siteTitle}}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
84
+ return `🙌 ${t(`Complete your sign up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
85
85
  case 'signup-paid':
86
- return `🙌 ${t(`Thank you for signing up to {{siteTitle}}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
86
+ return `🙌 ${t(`Thank you for signing up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
87
87
  case 'updateEmail':
88
- return `📫 ${t(`Confirm your email update for {{siteTitle}}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
88
+ return `📫 ${t(`Confirm your email update for {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
89
89
  case 'signin':
90
90
  default:
91
- return `🔑 ${t(`Secure sign in link for {{siteTitle}}`, {siteTitle, interpolation: {escapeValue: false}})}`;
91
+ return `🔑 ${t(`Secure sign in link for {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`;
92
92
  }
93
93
  },
94
94
  getText(url, type, email) {
@@ -98,7 +98,7 @@ function createApiInstance(config) {
98
98
  return trimLeadingWhitespace`
99
99
  ${t(`Hey there,`)}
100
100
 
101
- ${t('You\'re one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:', {siteTitle, interpolation: {escapeValue: false}})}
101
+ ${t('You\'re one tap away from subscribing to {siteTitle} — please confirm your email address with this link:', {siteTitle, interpolation: {escapeValue: false}})}
102
102
 
103
103
  ${url}
104
104
 
@@ -108,14 +108,14 @@ function createApiInstance(config) {
108
108
 
109
109
  ---
110
110
 
111
- ${t('Sent to {{email}}', {email})}
111
+ ${t('Sent to {email}', {email})}
112
112
  ${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be subscribed.')}
113
113
  `;
114
114
  case 'signup':
115
115
  return trimLeadingWhitespace`
116
116
  ${t(`Hey there,`)}
117
117
 
118
- ${t('Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}
118
+ ${t('Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}
119
119
 
120
120
  ${url}
121
121
 
@@ -125,14 +125,14 @@ function createApiInstance(config) {
125
125
 
126
126
  ---
127
127
 
128
- ${t('Sent to {{email}}', {email})}
128
+ ${t('Sent to {email}', {email})}
129
129
  ${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be signed up, and no account will be created for you.')}
130
130
  `;
131
131
  case 'signup-paid':
132
132
  return trimLeadingWhitespace`
133
133
  ${t(`Hey there,`)}
134
134
 
135
- ${t('Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}
135
+ ${t('Thank you for subscribing to {siteTitle}. Tap the link below to be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}
136
136
 
137
137
  ${url}
138
138
 
@@ -142,8 +142,8 @@ function createApiInstance(config) {
142
142
 
143
143
  ---
144
144
 
145
- ${t('Sent to {{email}}', {email})}
146
- ${t('Thank you for subscribing to {{siteTitle}}!', {siteTitle, interpolation: {escapeValue: false}})}
145
+ ${t('Sent to {email}', {email})}
146
+ ${t('Thank you for subscribing to {siteTitle}!', {siteTitle, interpolation: {escapeValue: false}})}
147
147
  `;
148
148
  case 'updateEmail':
149
149
  return trimLeadingWhitespace`
@@ -157,7 +157,7 @@ function createApiInstance(config) {
157
157
 
158
158
  ---
159
159
 
160
- ${t('Sent to {{email}}', {email})}
160
+ ${t('Sent to {email}', {email})}
161
161
  ${t('If you did not make this request, you can simply delete this message.')} ${t('This email address will not be used.')}
162
162
  `;
163
163
  case 'signin':
@@ -165,7 +165,7 @@ function createApiInstance(config) {
165
165
  return trimLeadingWhitespace`
166
166
  ${t(`Hey there,`)}
167
167
 
168
- ${t('Welcome back! Use this link to securely sign in to your {{siteTitle}} account:', {siteTitle, interpolation: {escapeValue: false}})}
168
+ ${t('Welcome back! Use this link to securely sign in to your {siteTitle} account:', {siteTitle, interpolation: {escapeValue: false}})}
169
169
 
170
170
  ${url}
171
171
 
@@ -175,7 +175,7 @@ function createApiInstance(config) {
175
175
 
176
176
  ---
177
177
 
178
- ${t('Sent to {{email}}', {email})}
178
+ ${t('Sent to {email}', {email})}
179
179
  ${t('If you did not make this request, you can safely ignore this email.')}
180
180
  `;
181
181
  }
@@ -4,7 +4,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
4
4
  <head>
5
5
  <meta name="viewport" content="width=device-width">
6
6
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
- <title>🔑 ${t('Secure sign in link for {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</title>
7
+ <title>🔑 ${t('Secure sign in link for {siteTitle}', {siteTitle, interpolation: {escapeValue: false}})}</title>
8
8
  <style>
9
9
  /* -------------------------------------
10
10
  RESPONSIVE AND MOBILE FRIENDLY STYLES
@@ -107,7 +107,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
107
107
  <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
108
108
 
109
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('Welcome back to {{siteTitle}}!', {siteTitle, interpolation: {escapeValue: false}})}</span>
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('Welcome back to {siteTitle}!', {siteTitle, interpolation: {escapeValue: false}})}</span>
111
111
  <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
112
112
 
113
113
  <!-- START MAIN CONTENT AREA -->
@@ -117,7 +117,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
117
117
  <tr>
118
118
  <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;">
119
119
  <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: bold; line-height: 24px; margin: 0; margin-bottom: 15px;">${t('Hey there,')}</p>
120
- <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;">${t('Welcome back! Use this link to securely sign in to your {{siteTitle}} account:', {siteTitle, interpolation: {escapeValue: false}})}</p>
120
+ <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;">${t('Welcome back! Use this link to securely sign in to your {siteTitle} account:', {siteTitle, interpolation: {escapeValue: false}})}</p>
121
121
  <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
122
122
  <tbody>
123
123
  <tr>
@@ -125,7 +125,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
125
125
  <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
126
126
  <tbody>
127
127
  <tr>
128
- <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: 16px; vertical-align: top; background-color: ${accentColor}; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: ${accentColor}; border: solid 1px ${accentColor}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: ${accentColor};">${t('Sign in to {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</a> </td>
128
+ <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: 16px; vertical-align: top; background-color: ${accentColor}; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: ${accentColor}; border: solid 1px ${accentColor}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: ${accentColor};">${t('Sign in to {siteTitle}', {siteTitle, interpolation: {escapeValue: false}})}</a> </td>
129
129
  </tr>
130
130
  </tbody>
131
131
  </table>
@@ -4,7 +4,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
4
4
  <head>
5
5
  <meta name="viewport" content="width=device-width">
6
6
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
- <title>🙌 ${t('Thank you for signing up to {{siteTitle}}!', {siteTitle, interpolation: {escapeValue: false}})}</title>
7
+ <title>🙌 ${t('Thank you for signing up to {siteTitle}!', {siteTitle, interpolation: {escapeValue: false}})}</title>
8
8
  <style>
9
9
  /* -------------------------------------
10
10
  RESPONSIVE AND MOBILE FRIENDLY STYLES
@@ -107,7 +107,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
107
107
  <div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 30px 20px;">
108
108
 
109
109
  <!-- START CENTERED WHITE 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('Thank you for subscribing to {{siteTitle}}.', {siteTitle, interpolation: {escapeValue: false}})}</span>
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('Thank you for subscribing to {siteTitle}.', {siteTitle, interpolation: {escapeValue: false}})}</span>
111
111
  <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
112
112
 
113
113
  <!-- START MAIN CONTENT AREA -->
@@ -117,7 +117,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
117
117
  <tr>
118
118
  <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;">
119
119
  <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: bold; line-height: 24px; margin: 0; margin-bottom: 15px;">${t('Hey there!')}</p>
120
- <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: 32px;">${t('Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}</p>
120
+ <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: 32px;">${t('Thank you for subscribing to {siteTitle}. Tap the link below to be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}</p>
121
121
  <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
122
122
  <tbody>
123
123
  <tr>
@@ -165,4 +165,3 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
165
165
  </body>
166
166
  </html>
167
167
  `;
168
-
@@ -4,7 +4,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
4
4
  <head>
5
5
  <meta name="viewport" content="width=device-width">
6
6
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
- <title>🙌 ${t('Complete your sign up to {{siteTitle}}!', {siteTitle, interpolation: {escapeValue: false}})}</title>
7
+ <title>🙌 ${t('Complete your sign up to {siteTitle}!', {siteTitle, interpolation: {escapeValue: false}})}</title>
8
8
  <style>
9
9
  /* -------------------------------------
10
10
  RESPONSIVE AND MOBILE FRIENDLY STYLES
@@ -107,7 +107,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
107
107
  <div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 30px 20px;">
108
108
 
109
109
  <!-- START CENTERED WHITE 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('Complete signup for {{siteTitle}}!', {siteTitle, interpolation: {escapeValue: false}})}</span>
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('Complete signup for {siteTitle}!', {siteTitle, interpolation: {escapeValue: false}})}</span>
111
111
  <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
112
112
 
113
113
  <!-- START MAIN CONTENT AREA -->
@@ -117,7 +117,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
117
117
  <tr>
118
118
  <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;">
119
119
  <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: bold; line-height: 24px; margin: 0; margin-bottom: 15px;">${t('Hey there!')}</p>
120
- <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: 32px;">${t('Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}</p>
120
+ <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: 32px;">${t('Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}</p>
121
121
  <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
122
122
  <tbody>
123
123
  <tr>
@@ -4,7 +4,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
4
4
  <head>
5
5
  <meta name="viewport" content="width=device-width">
6
6
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
- <title>📫 ${t('Confirm your subscription to {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</title>
7
+ <title>📫 ${t('Confirm your subscription to {siteTitle}', {siteTitle, interpolation: {escapeValue: false}})}</title>
8
8
  <style>
9
9
  /* -------------------------------------
10
10
  RESPONSIVE AND MOBILE FRIENDLY STYLES
@@ -107,7 +107,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
107
107
  <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
108
108
 
109
109
  <!-- START CENTERED WHITE 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(`You're one tap away from subscribing to {{siteTitle}}!`, {siteTitle, interpolation: {escapeValue: false}})}</span>
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(`You're one tap away from subscribing to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}</span>
111
111
  <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
112
112
 
113
113
  <!-- START MAIN CONTENT AREA -->
@@ -117,7 +117,7 @@ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain
117
117
  <tr>
118
118
  <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;">
119
119
  <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: bold; line-height: 24px; margin: 0; margin-bottom: 15px;">${t('Hey there,')}</p>
120
- <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;">${t(`You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:`, {siteTitle, interpolation: {escapeValue: false}})}</p>
120
+ <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;">${t(`You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:`, {siteTitle, interpolation: {escapeValue: false}})}</p>
121
121
  <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
122
122
  <tbody>
123
123
  <tr>
@@ -369,9 +369,9 @@ module.exports = class RouterController {
369
369
 
370
370
  if (member) {
371
371
  options.successUrl = this._generateSuccessUrl(options.successUrl, tier.welcomePageURL);
372
-
372
+
373
373
  const restrictCheckout = member.get('status') === 'paid';
374
-
374
+
375
375
  if (restrictCheckout) {
376
376
  // This member is already subscribed to a paid tier
377
377
  // We don't want to create a duplicate subscription
@@ -413,17 +413,17 @@ module.exports = class RouterController {
413
413
  try {
414
414
  // Create URL objects
415
415
  const siteUrl = this._urlUtils.getSiteUrl();
416
-
416
+
417
417
  // This will throw if welcomePageURL is invalid
418
418
  const welcomeUrl = new URL(
419
- welcomePageURL.startsWith('http') ? welcomePageURL : welcomePageURL,
419
+ welcomePageURL.startsWith('http') ? welcomePageURL : welcomePageURL,
420
420
  siteUrl
421
421
  );
422
-
422
+
423
423
  // Add success parameters
424
424
  welcomeUrl.searchParams.set('success', 'true');
425
425
  welcomeUrl.searchParams.set('action', 'signup');
426
-
426
+
427
427
  return welcomeUrl.href;
428
428
  } catch (err) {
429
429
  logging.warn(`Invalid welcome page URL "${welcomePageURL}", using original success URL`, err);
@@ -509,6 +509,10 @@ module.exports = class RouterController {
509
509
  // Store attribution data in the metadata
510
510
  await this._setAttributionMetadata(metadata);
511
511
 
512
+ if (metadata.newsletters) {
513
+ metadata.newsletters = JSON.stringify(await this._validateNewsletters(JSON.parse(metadata.newsletters)));
514
+ }
515
+
512
516
  // Build options
513
517
  const options = {
514
518
  successUrl: req.body.successUrl,
@@ -649,7 +653,7 @@ module.exports = class RouterController {
649
653
  labels: req.body.labels,
650
654
  name: req.body.name,
651
655
  reqIp: req.ip ?? undefined,
652
- newsletters: await this._validateNewsletters(req),
656
+ newsletters: await this._validateNewsletters(req.body?.newsletters ?? []),
653
657
  attribution: await this._memberAttributionService.getAttribution(req.body.urlHistory)
654
658
  };
655
659
 
@@ -671,42 +675,52 @@ module.exports = class RouterController {
671
675
  return await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, referrer});
672
676
  }
673
677
 
674
- async _validateNewsletters(req) {
675
- const {newsletters: requestedNewsletters} = req.body;
678
+ /**
679
+ * Validates the newsletters in the request body
680
+ * @param {object[]} requestedNewsletters
681
+ * @param {string} requestedNewsletters[].name
682
+ * @returns {Promise<object[] | undefined>} The validated newsletters
683
+ */
684
+ async _validateNewsletters(requestedNewsletters) {
685
+ if (!requestedNewsletters || requestedNewsletters.length === 0) {
686
+ return undefined;
687
+ }
676
688
 
677
- if (requestedNewsletters && requestedNewsletters.length > 0 && requestedNewsletters.every(newsletter => newsletter.name !== undefined)) {
678
- const newsletterNames = requestedNewsletters.map(newsletter => newsletter.name);
679
- const newsletterNamesFilter = newsletterNames.map(newsletter => `'${newsletter.replace(/("|')/g, '\\$1')}'`);
680
- const newsletters = (await this._newslettersService.getAll({
681
- filter: `name:[${newsletterNamesFilter}]`,
682
- columns: ['id','name','status']
683
- }));
689
+ if (requestedNewsletters.some(newsletter => !newsletter.name)) {
690
+ return undefined;
691
+ }
684
692
 
685
- // Check for invalid newsletters
686
- if (newsletters.length !== newsletterNames.length) {
687
- const validNewsletters = newsletters.map(newsletter => newsletter.name);
688
- const invalidNewsletters = newsletterNames.filter(newsletter => !validNewsletters.includes(newsletter));
693
+ const requestedNewsletterNames = requestedNewsletters.map(newsletter => newsletter.name);
694
+ const requestedNewsletterNamesFilter = requestedNewsletterNames.map(newsletter => `'${newsletter.replace(/("|')/g, '\\$1')}'`);
695
+ const matchedNewsletters = (await this._newslettersService.getAll({
696
+ filter: `name:[${requestedNewsletterNamesFilter}]`,
697
+ columns: ['id','name','status']
698
+ }));
689
699
 
690
- throw new errors.BadRequestError({
691
- message: tpl(messages.invalidNewsletters, {newsletters: invalidNewsletters})
692
- });
693
- }
700
+ // Check for invalid newsletters
701
+ if (matchedNewsletters.length !== requestedNewsletterNames.length) {
702
+ const validNewsletterNames = matchedNewsletters.map(newsletter => newsletter.name);
703
+ const invalidNewsletterNames = requestedNewsletterNames.filter(newsletter => !validNewsletterNames.includes(newsletter));
694
704
 
695
- // Check for archived newsletters
696
- const archivedNewsletters = newsletters
697
- .filter(newsletter => newsletter.status === 'archived')
698
- .map(newsletter => newsletter.name);
705
+ throw new errors.BadRequestError({
706
+ message: tpl(messages.invalidNewsletters, {newsletters: invalidNewsletterNames})
707
+ });
708
+ }
699
709
 
700
- if (archivedNewsletters && archivedNewsletters.length > 0) {
701
- throw new errors.BadRequestError({
702
- message: tpl(messages.archivedNewsletters, {newsletters: archivedNewsletters})
703
- });
704
- }
710
+ // Check for archived newsletters
711
+ const requestedArchivedNewsletters = matchedNewsletters
712
+ .filter(newsletter => newsletter.status === 'archived')
713
+ .map(newsletter => newsletter.name);
705
714
 
706
- return newsletters
707
- .filter(newsletter => newsletter.status === 'active')
708
- .map(newsletter => ({id: newsletter.id}));
715
+ if (requestedArchivedNewsletters && requestedArchivedNewsletters.length > 0) {
716
+ throw new errors.BadRequestError({
717
+ message: tpl(messages.archivedNewsletters, {newsletters: requestedArchivedNewsletters})
718
+ });
709
719
  }
720
+
721
+ return matchedNewsletters
722
+ .filter(newsletter => newsletter.status === 'active')
723
+ .map(newsletter => ({id: newsletter.id}));
710
724
  }
711
725
  };
712
726