ghost 5.117.0 → 5.118.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 (124) hide show
  1. package/components/{tryghost-api-framework-5.117.0.tgz → tryghost-api-framework-5.118.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.118.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.118.0.tgz +0 -0
  4. package/components/tryghost-custom-theme-settings-service-5.118.0.tgz +0 -0
  5. package/components/tryghost-domain-events-5.118.0.tgz +0 -0
  6. package/components/tryghost-donations-5.118.0.tgz +0 -0
  7. package/components/{tryghost-email-addresses-5.117.0.tgz → tryghost-email-addresses-5.118.0.tgz} +0 -0
  8. package/components/{tryghost-email-service-5.117.0.tgz → tryghost-email-service-5.118.0.tgz} +0 -0
  9. package/components/{tryghost-email-suppression-list-5.117.0.tgz → tryghost-email-suppression-list-5.118.0.tgz} +0 -0
  10. package/components/tryghost-html-to-plaintext-5.118.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.118.0.tgz +0 -0
  12. package/components/{tryghost-job-manager-5.117.0.tgz → tryghost-job-manager-5.118.0.tgz} +0 -0
  13. package/components/tryghost-link-replacer-5.118.0.tgz +0 -0
  14. package/components/{tryghost-magic-link-5.117.0.tgz → tryghost-magic-link-5.118.0.tgz} +0 -0
  15. package/components/{tryghost-member-attribution-5.117.0.tgz → tryghost-member-attribution-5.118.0.tgz} +0 -0
  16. package/components/tryghost-member-events-5.118.0.tgz +0 -0
  17. package/components/{tryghost-members-csv-5.117.0.tgz → tryghost-members-csv-5.118.0.tgz} +0 -0
  18. package/components/{tryghost-members-offers-5.117.0.tgz → tryghost-members-offers-5.118.0.tgz} +0 -0
  19. package/components/tryghost-mw-error-handler-5.118.0.tgz +0 -0
  20. package/components/tryghost-mw-vhost-5.118.0.tgz +0 -0
  21. package/components/tryghost-post-events-5.118.0.tgz +0 -0
  22. package/components/tryghost-post-revisions-5.118.0.tgz +0 -0
  23. package/components/{tryghost-posts-service-5.117.0.tgz → tryghost-posts-service-5.118.0.tgz} +0 -0
  24. package/components/tryghost-prometheus-metrics-5.118.0.tgz +0 -0
  25. package/components/tryghost-security-5.118.0.tgz +0 -0
  26. package/components/tryghost-tiers-5.118.0.tgz +0 -0
  27. package/components/tryghost-webmentions-5.118.0.tgz +0 -0
  28. package/content/themes/casper/LICENSE +1 -1
  29. package/content/themes/casper/README.md +1 -1
  30. package/content/themes/casper/assets/built/screen.css +1 -1
  31. package/content/themes/casper/assets/built/screen.css.map +1 -1
  32. package/content/themes/casper/assets/css/screen.css +1 -1
  33. package/content/themes/casper/author.hbs +23 -2
  34. package/content/themes/casper/package.json +2 -2
  35. package/content/themes/casper/partials/icons/bluesky.hbs +3 -0
  36. package/content/themes/casper/partials/icons/instagram.hbs +5 -0
  37. package/content/themes/casper/partials/icons/linkedin.hbs +3 -0
  38. package/content/themes/casper/partials/icons/mastodon.hbs +3 -0
  39. package/content/themes/casper/partials/icons/threads.hbs +3 -0
  40. package/content/themes/casper/partials/icons/tiktok.hbs +3 -0
  41. package/content/themes/casper/partials/icons/twitter.hbs +3 -1
  42. package/content/themes/casper/partials/icons/youtube.hbs +3 -0
  43. package/content/themes/source/LICENSE +1 -1
  44. package/content/themes/source/README.md +1 -1
  45. package/content/themes/source/assets/built/screen.css +1 -1
  46. package/content/themes/source/assets/built/screen.css.map +1 -1
  47. package/content/themes/source/assets/css/screen.css +7 -12
  48. package/content/themes/source/author.hbs +24 -3
  49. package/content/themes/source/package.json +2 -2
  50. package/content/themes/source/partials/feature-image.hbs +2 -2
  51. package/content/themes/source/partials/icons/bluesky.hbs +3 -0
  52. package/content/themes/source/partials/icons/instagram.hbs +5 -0
  53. package/content/themes/source/partials/icons/linkedin.hbs +3 -0
  54. package/content/themes/source/partials/icons/mastodon.hbs +3 -0
  55. package/content/themes/source/partials/icons/threads.hbs +3 -0
  56. package/content/themes/source/partials/icons/tiktok.hbs +3 -0
  57. package/content/themes/source/partials/icons/youtube.hbs +3 -0
  58. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +31773 -26586
  59. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-3bc05d1b.mjs → CodeEditorView-1143c509.mjs} +2 -2
  60. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  61. package/core/built/admin/assets/admin-x-settings/{index-c5623145.mjs → index-19ebc8ad.mjs} +2 -2
  62. package/core/built/admin/assets/admin-x-settings/{index-b2cdc747.mjs → index-ac104f42.mjs} +2559 -2551
  63. package/core/built/admin/assets/admin-x-settings/{modals-fd7bc70c.mjs → modals-994901ee.mjs} +6680 -6165
  64. package/core/built/admin/assets/{chunk.524.f4d7526780f546c5fc0b.js → chunk.524.5710919eb507b9a81166.js} +6 -6
  65. package/core/built/admin/assets/{chunk.582.869c66dfbfa68412de07.js → chunk.582.c8cb99b85cfa13fc7df1.js} +8 -8
  66. package/core/built/admin/assets/{ghost-45186e4f079c9fdd8f42dfbfb93d3344.js → ghost-cd90a28b214ee800a007bb62cd45e6e6.js} +803 -800
  67. package/core/built/admin/assets/posts/posts.js +11542 -11341
  68. package/core/built/admin/assets/stats/stats.js +37309 -31520
  69. package/core/built/admin/index.html +3 -3
  70. package/core/frontend/helpers/social_url.js +31 -0
  71. package/core/server/api/endpoints/users.js +7 -0
  72. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  73. package/core/server/services/auth/session/index.js +5 -2
  74. package/core/server/services/auth/session/session-service.js +5 -4
  75. package/core/server/services/members/api.js +2 -2
  76. package/core/server/services/members/members-api/controllers/MemberController.js +214 -0
  77. package/core/server/services/members/members-api/controllers/RouterController.js +667 -0
  78. package/core/server/services/members/members-api/controllers/WellKnownController.js +46 -0
  79. package/core/server/services/members/members-api/members-api.js +404 -0
  80. package/core/server/services/members/members-api/repositories/EventRepository.js +984 -0
  81. package/core/server/services/members/members-api/repositories/MemberRepository.js +1739 -0
  82. package/core/server/services/members/members-api/repositories/ProductRepository.js +662 -0
  83. package/core/server/services/members/members-api/services/GeolocationService.js +23 -0
  84. package/core/server/services/members/members-api/services/MemberBREADService.js +444 -0
  85. package/core/server/services/members/members-api/services/PaymentsService.js +522 -0
  86. package/core/server/services/members/members-api/services/TokenService.js +54 -0
  87. package/core/server/services/milestones/BookshelfMilestoneRepository.js +8 -9
  88. package/core/server/services/milestones/InMemoryMilestoneRepository.js +119 -0
  89. package/core/server/services/milestones/Milestone.js +231 -0
  90. package/core/server/services/milestones/MilestoneCreatedEvent.js +22 -0
  91. package/core/server/services/milestones/MilestonesService.js +327 -0
  92. package/core/server/services/milestones/service.js +2 -2
  93. package/core/server/services/newsletters/index.js +1 -1
  94. package/core/server/services/public-config/config.js +2 -1
  95. package/core/server/services/settings/settings-service.js +1 -1
  96. package/core/server/services/slack-notifications/SlackNotifications.js +1 -1
  97. package/core/server/services/slack-notifications/SlackNotificationsService.js +2 -2
  98. package/core/server/services/staff/StaffService.js +1 -1
  99. package/core/shared/config/defaults.json +3 -0
  100. package/core/shared/config/env/config.testing-mysql.json +3 -0
  101. package/core/shared/config/env/config.testing.json +3 -0
  102. package/core/shared/labs.js +3 -4
  103. package/package.json +63 -63
  104. package/tsconfig.tsbuildinfo +1 -1
  105. package/yarn.lock +86 -44
  106. package/components/tryghost-constants-5.117.0.tgz +0 -0
  107. package/components/tryghost-custom-fonts-5.117.0.tgz +0 -0
  108. package/components/tryghost-custom-theme-settings-service-5.117.0.tgz +0 -0
  109. package/components/tryghost-domain-events-5.117.0.tgz +0 -0
  110. package/components/tryghost-donations-5.117.0.tgz +0 -0
  111. package/components/tryghost-html-to-plaintext-5.117.0.tgz +0 -0
  112. package/components/tryghost-i18n-5.117.0.tgz +0 -0
  113. package/components/tryghost-link-replacer-5.117.0.tgz +0 -0
  114. package/components/tryghost-member-events-5.117.0.tgz +0 -0
  115. package/components/tryghost-members-api-5.117.0.tgz +0 -0
  116. package/components/tryghost-milestones-5.117.0.tgz +0 -0
  117. package/components/tryghost-mw-error-handler-5.117.0.tgz +0 -0
  118. package/components/tryghost-mw-vhost-5.117.0.tgz +0 -0
  119. package/components/tryghost-post-events-5.117.0.tgz +0 -0
  120. package/components/tryghost-post-revisions-5.117.0.tgz +0 -0
  121. package/components/tryghost-prometheus-metrics-5.117.0.tgz +0 -0
  122. package/components/tryghost-security-5.117.0.tgz +0 -0
  123. package/components/tryghost-tiers-5.117.0.tgz +0 -0
  124. package/components/tryghost-webmentions-5.117.0.tgz +0 -0
@@ -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.117%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%2270b6e1c5ab%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%22fd54878445%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22cbac8e867a%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22e88b5b6865%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%226845f0f047%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%2203ed60fb0a%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
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.118%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%2270b6e1c5ab%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%22fd54878445%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22dfc54ca9f9%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%2245376f8ac6%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%222be41c1d8b%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%22a6a4e7e500%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -58,7 +58,7 @@
58
58
 
59
59
  <script src="assets/vendor-8e3ee8261528bb429cfe78ce79a4a82a.js"></script>
60
60
  <script src="assets/chunk.713.48f120c377bcaffdfddf.js"></script>
61
- <script src="assets/chunk.524.f4d7526780f546c5fc0b.js"></script>
62
- <script src="assets/ghost-45186e4f079c9fdd8f42dfbfb93d3344.js"></script>
61
+ <script src="assets/chunk.524.5710919eb507b9a81166.js"></script>
62
+ <script src="assets/ghost-cd90a28b214ee800a007bb62cd45e6e6.js"></script>
63
63
  </body>
64
64
  </html>
@@ -0,0 +1,31 @@
1
+ // # Social URL Helper
2
+ // Usage: `{{social_url type="platform"}}` (e.g., type="facebook", type="twitter")
3
+ //
4
+ // Output a url for a social media username defined in site settings.
5
+ const {socialUrls} = require('../services/proxy');
6
+ const {localUtils} = require('../services/handlebars');
7
+
8
+ // We use the name social_url to match the helper for consistency:
9
+ module.exports = function social_url(options) { // eslint-disable-line camelcase
10
+ // Check for required hash option 'type'
11
+ if (!options || !options.hash || !options.hash.type) {
12
+ return null;
13
+ }
14
+
15
+ const platform = options.hash.type;
16
+ const siteData = options.data && options.data.site;
17
+
18
+ if (!siteData) {
19
+ return null;
20
+ }
21
+
22
+ // Use localUtils.findKey for potential context fallback, though siteData is primary
23
+ const username = localUtils.findKey(platform, this, siteData);
24
+
25
+ // Check if the platform is supported by socialUrls and the username exists
26
+ if (username && typeof socialUrls[platform] === 'function') {
27
+ return socialUrls[platform](username);
28
+ }
29
+
30
+ return null;
31
+ };
@@ -53,6 +53,13 @@ function shouldInvalidateCacheAfterChange(model) {
53
53
  'location',
54
54
  'facebook',
55
55
  'twitter',
56
+ 'mastodon',
57
+ 'youtube',
58
+ 'linkedin',
59
+ 'bluesky',
60
+ 'instagram',
61
+ 'tiktok',
62
+ 'threads',
56
63
  'status',
57
64
  'visibility',
58
65
  'meta_title',
@@ -21,7 +21,8 @@ module.exports = {
21
21
  'tenor',
22
22
  'pintura',
23
23
  'signupForm',
24
- 'stats'
24
+ 'stats',
25
+ 'security'
25
26
  ];
26
27
 
27
28
  frame.response = {
@@ -5,12 +5,12 @@ const createSessionMiddleware = require('./middleware');
5
5
  const settingsCache = require('../../../../shared/settings-cache');
6
6
  const {GhostMailer} = require('../../mail');
7
7
  const {t} = require('../../i18n');
8
- const labs = require('../../../../shared/labs');
9
8
 
10
9
  const expressSession = require('./express-session');
11
10
 
12
11
  const models = require('../../../models');
13
12
  const urlUtils = require('../../../../shared/url-utils');
13
+ const config = require('../../../../shared/config');
14
14
  const {blogIcon} = require('../../../lib/image');
15
15
  const url = require('url');
16
16
 
@@ -47,13 +47,16 @@ const sessionService = createSessionService({
47
47
  getSettingsCache(key) {
48
48
  return settingsCache.get(key);
49
49
  },
50
+ isStaffDeviceVerificationDisabled() {
51
+ // This config flag is set to true by default, so we need to check for false
52
+ return config.get('security:staffDeviceVerification') !== true;
53
+ },
50
54
  getBlogLogo() {
51
55
  return blogIcon.getIconUrl({absolute: true, fallbackToDefault: false})
52
56
  || 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png';
53
57
  },
54
58
  mailer,
55
59
  urlUtils,
56
- labs,
57
60
  t
58
61
  });
59
62
 
@@ -57,9 +57,9 @@ totp.options = {
57
57
  * @param {(key: 'require_email_mfa' | 'admin_session_secret' | 'title') => boolean | string} deps.getSettingsCache
58
58
  * @param {() => string} deps.getBlogLogo
59
59
  * @param {import('../../core/core/server/services/mail').GhostMailer} deps.mailer
60
- * @param {import('../../core/core/shared/labs')} deps.labs
61
60
  * @param {import('../../core/core/server/services/i18n').t} deps.t
62
61
  * @param {import('../../core/core/shared/url-utils')} deps.urlUtils
62
+ * @param {() => boolean} deps.isStaffDeviceVerificationDisabled
63
63
  * @returns {SessionService}
64
64
  */
65
65
 
@@ -71,7 +71,7 @@ module.exports = function createSessionService({
71
71
  getBlogLogo,
72
72
  mailer,
73
73
  urlUtils,
74
- labs,
74
+ isStaffDeviceVerificationDisabled,
75
75
  t
76
76
  }) {
77
77
  /**
@@ -128,7 +128,7 @@ module.exports = function createSessionService({
128
128
  session.user_agent = req.get('user-agent');
129
129
  session.ip = req.ip;
130
130
 
131
- if (!labs.isSet('staff2fa')) {
131
+ if (isStaffDeviceVerificationDisabled()) {
132
132
  session.verified = true;
133
133
  }
134
134
  }
@@ -260,7 +260,7 @@ module.exports = function createSessionService({
260
260
  const siteTitle = getSettingsCache('title');
261
261
  const siteLogo = getBlogLogo();
262
262
  const siteUrl = urlUtils.urlFor('home', true);
263
- const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
263
+ const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)','i'));
264
264
  const siteDomain = (domain && domain[1]);
265
265
  const email = emailTemplate({
266
266
  t,
@@ -370,5 +370,6 @@ module.exports = function createSessionService({
370
370
  verifyAuthCodeForUser,
371
371
  generateAuthCodeForUser,
372
372
  isVerificationRequired
373
+
373
374
  };
374
375
  };
@@ -1,7 +1,7 @@
1
1
  const stripeService = require('../stripe');
2
2
  const settingsCache = require('../../../shared/settings-cache');
3
3
  const settingsHelpers = require('../../services/settings-helpers');
4
- const MembersApi = require('@tryghost/members-api');
4
+ const MembersApi = require('./members-api/members-api');
5
5
  const logging = require('@tryghost/logging');
6
6
  const mail = require('../mail');
7
7
  const models = require('../../models');
@@ -25,7 +25,7 @@ const sharedConfig = require('../../../shared/config');
25
25
 
26
26
  const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
27
27
  const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000;
28
- const MAGIC_LINK_TOKEN_MAX_USAGE_COUNT = 3;
28
+ const MAGIC_LINK_TOKEN_MAX_USAGE_COUNT = 7;
29
29
 
30
30
  const ghostMailer = new mail.GhostMailer();
31
31
 
@@ -0,0 +1,214 @@
1
+ const errors = require('@tryghost/errors');
2
+ const tpl = require('@tryghost/tpl');
3
+
4
+ const messages = {
5
+ blockedEmailDomain: 'Memberships from this email domain are currently restricted.'
6
+ };
7
+
8
+ module.exports = class MemberController {
9
+ /**
10
+ * @param {object} deps
11
+ * @param {any} deps.memberRepository
12
+ * @param {any} deps.productRepository
13
+ * @param {any} deps.paymentsService
14
+ * @param {any} deps.tiersService
15
+ * @param {any} deps.StripePrice
16
+ * @param {any} deps.tokenService
17
+ * @param {any} deps.sendEmailWithMagicLink
18
+ * @param {any} deps.settingsCache
19
+ */
20
+ constructor({
21
+ memberRepository,
22
+ productRepository,
23
+ paymentsService,
24
+ tiersService,
25
+ StripePrice,
26
+ tokenService,
27
+ sendEmailWithMagicLink,
28
+ settingsCache
29
+ }) {
30
+ this._memberRepository = memberRepository;
31
+ this._productRepository = productRepository;
32
+ this._paymentsService = paymentsService;
33
+ this._tiersService = tiersService;
34
+ this._StripePrice = StripePrice;
35
+ this._tokenService = tokenService;
36
+ this._sendEmailWithMagicLink = sendEmailWithMagicLink;
37
+ this._settingsCache = settingsCache;
38
+ }
39
+
40
+ async updateEmailAddress(req, res) {
41
+ const identity = req.body.identity;
42
+ const email = req.body.email;
43
+ const options = {
44
+ forceEmailType: true
45
+ };
46
+
47
+ if (!identity) {
48
+ res.writeHead(403);
49
+ return res.end('No Permission.');
50
+ }
51
+
52
+ const blockedEmailDomains = this._settingsCache.get('all_blocked_email_domains');
53
+ const emailDomain = req.body.email.split('@')[1]?.toLowerCase();
54
+ if (emailDomain && blockedEmailDomains.includes(emailDomain)) {
55
+ throw new errors.BadRequestError({
56
+ message: tpl(messages.blockedEmailDomain)
57
+ });
58
+ }
59
+
60
+ let tokenData = {};
61
+ try {
62
+ const member = await this._memberRepository.getByToken(identity);
63
+ tokenData.oldEmail = member.get('email');
64
+ } catch (err) {
65
+ res.writeHead(401);
66
+ return res.end('Unauthorized.');
67
+ }
68
+
69
+ try {
70
+ await this._sendEmailWithMagicLink({email, tokenData, requestedType: 'updateEmail', options});
71
+ res.writeHead(201);
72
+ return res.end('Created.');
73
+ } catch (err) {
74
+ res.writeHead(500);
75
+ return res.end('Internal Server Error.');
76
+ }
77
+ }
78
+
79
+ async updateSubscription(req, res) {
80
+ try {
81
+ const identity = req.body.identity;
82
+ const subscriptionId = req.params.id;
83
+ const cancelAtPeriodEnd = req.body.cancel_at_period_end;
84
+ const smartCancel = req.body.smart_cancel;
85
+ const cancellationReason = req.body.cancellation_reason;
86
+ let ghostPriceId = req.body.priceId;
87
+ const tierId = req.body.tierId;
88
+ const cadence = req.body.cadence;
89
+
90
+ if (cancelAtPeriodEnd === undefined && ghostPriceId === undefined && smartCancel === undefined && tierId === undefined && cadence === undefined) {
91
+ throw new errors.BadRequestError({
92
+ message: 'Updating subscription failed!',
93
+ help: 'Request should contain "cancel_at_period_end" or "priceId" or "smart_cancel" field.'
94
+ });
95
+ }
96
+
97
+ if ((cancelAtPeriodEnd === undefined || cancelAtPeriodEnd === false) && !smartCancel && cancellationReason !== undefined) {
98
+ throw new errors.BadRequestError({
99
+ message: 'Updating subscription failed!',
100
+ help: '"cancellation_reason" field requires the "cancel_at_period_end" or "smart_cancel" field to be true.'
101
+ });
102
+ }
103
+
104
+ if (cancellationReason && cancellationReason.length > 500) {
105
+ throw new errors.BadRequestError({
106
+ message: 'Updating subscription failed!',
107
+ help: '"cancellation_reason" field can be a maximum of 500 characters.'
108
+ });
109
+ }
110
+
111
+ let email;
112
+ try {
113
+ if (!identity) {
114
+ throw new errors.BadRequestError({
115
+ message: 'Updating subscription failed! Could not find member'
116
+ });
117
+ }
118
+
119
+ const claims = await this._tokenService.decodeToken(identity);
120
+ email = claims && claims.sub;
121
+ } catch (err) {
122
+ res.writeHead(401);
123
+ return res.end('Unauthorized');
124
+ }
125
+
126
+ if (!email) {
127
+ throw new errors.BadRequestError({
128
+ message: 'Invalid token'
129
+ });
130
+ }
131
+
132
+ if (tierId && cadence) {
133
+ const tier = await this._tiersService.api.read(tierId);
134
+ const stripePrice = await this._paymentsService.getPriceForTierCadence(tier, cadence);
135
+
136
+ await this._memberRepository.updateSubscription({
137
+ email,
138
+ subscription: {
139
+ subscription_id: subscriptionId,
140
+ price: stripePrice.id
141
+ }
142
+ });
143
+ } else if (ghostPriceId !== undefined) {
144
+ const price = await this._StripePrice.findOne({
145
+ id: ghostPriceId
146
+ });
147
+
148
+ if (!price) {
149
+ res.writeHead(404);
150
+ return res.end('Not Found.');
151
+ }
152
+
153
+ const priceId = price.get('stripe_price_id');
154
+ const product = await this._productRepository.get({stripe_price_id: priceId});
155
+
156
+ if (product.get('active') !== true) {
157
+ res.writeHead(403);
158
+ return res.end('Tier is archived.');
159
+ }
160
+
161
+ await this._memberRepository.updateSubscription({
162
+ email,
163
+ subscription: {
164
+ subscription_id: subscriptionId,
165
+ price: priceId
166
+ }
167
+ });
168
+ } else if (cancelAtPeriodEnd !== undefined) {
169
+ await this._memberRepository.updateSubscription({
170
+ email,
171
+ subscription: {
172
+ subscription_id: subscriptionId,
173
+ cancel_at_period_end: cancelAtPeriodEnd,
174
+ cancellationReason
175
+ }
176
+ });
177
+ } else if (smartCancel) {
178
+ const currentSubscription = await this._memberRepository.getSubscription({
179
+ email,
180
+ subscription: {
181
+ subscription_id: subscriptionId
182
+ }
183
+ });
184
+
185
+ if (['past_due', 'unpaid'].includes(currentSubscription.status)) {
186
+ await this._memberRepository.cancelSubscription({
187
+ email,
188
+ subscription: {
189
+ subscription_id: subscriptionId,
190
+ cancellationReason
191
+ }
192
+ });
193
+ } else {
194
+ await this._memberRepository.updateSubscription({
195
+ email,
196
+ subscription: {
197
+ subscription_id: subscriptionId,
198
+ cancel_at_period_end: true,
199
+ cancellationReason
200
+ }
201
+ });
202
+ }
203
+ }
204
+
205
+ res.writeHead(204);
206
+ res.end();
207
+ } catch (err) {
208
+ res.writeHead(err.statusCode || 500, {
209
+ 'Content-Type': 'text/plain;charset=UTF-8'
210
+ });
211
+ res.end(err.message);
212
+ }
213
+ }
214
+ };