ghost 5.116.2 → 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 (131) hide show
  1. package/components/{tryghost-api-framework-5.116.2.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.118.0.tgz +0 -0
  8. package/components/{tryghost-email-service-5.116.2.tgz → tryghost-email-service-5.118.0.tgz} +0 -0
  9. package/components/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.116.2.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.116.2.tgz → tryghost-magic-link-5.118.0.tgz} +0 -0
  15. package/components/{tryghost-member-attribution-5.116.2.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.116.2.tgz → tryghost-members-csv-5.118.0.tgz} +0 -0
  18. package/components/{tryghost-members-offers-5.116.2.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.116.2.tgz → 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.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 +31793 -26588
  59. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-550846e0.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-f3cb3f4d.mjs → index-19ebc8ad.mjs} +2 -2
  62. package/core/built/admin/assets/admin-x-settings/{index-4ce2fcd1.mjs → index-ac104f42.mjs} +2635 -2607
  63. package/core/built/admin/assets/admin-x-settings/{modals-6bc20529.mjs → modals-994901ee.mjs} +6680 -6165
  64. package/core/built/admin/assets/{chunk.524.578de86e5014b911b05a.js → chunk.524.5710919eb507b9a81166.js} +8 -8
  65. package/core/built/admin/assets/{chunk.582.21bf3e37b5d84ac4b58a.js → chunk.582.c8cb99b85cfa13fc7df1.js} +10 -10
  66. package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js → chunk.713.48f120c377bcaffdfddf.js} +6 -9
  67. package/core/built/admin/assets/{ghost-868c537d5c02ca65323d0122596a67ec.js → ghost-cd90a28b214ee800a007bb62cd45e6e6.js} +780 -775
  68. package/core/built/admin/assets/posts/posts.js +11561 -11302
  69. package/core/built/admin/assets/stats/stats.js +76076 -59355
  70. package/core/built/admin/index.html +4 -4
  71. package/core/frontend/helpers/social_url.js +31 -0
  72. package/core/server/api/endpoints/users.js +7 -0
  73. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  74. package/core/server/data/migrations/versions/5.117/2025-04-14-02-36-30-add-additional-social-accounts-columns-to-user-table.js +38 -0
  75. package/core/server/data/schema/schema.js +7 -0
  76. package/core/server/services/auth/session/index.js +5 -2
  77. package/core/server/services/auth/session/middleware.js +2 -1
  78. package/core/server/services/auth/session/session-service.js +7 -6
  79. package/core/server/services/members/api.js +2 -2
  80. package/core/server/services/members/members-api/controllers/MemberController.js +214 -0
  81. package/core/server/services/members/members-api/controllers/RouterController.js +667 -0
  82. package/core/server/services/members/members-api/controllers/WellKnownController.js +46 -0
  83. package/core/server/services/members/members-api/members-api.js +404 -0
  84. package/core/server/services/members/members-api/repositories/EventRepository.js +984 -0
  85. package/core/server/services/members/members-api/repositories/MemberRepository.js +1739 -0
  86. package/core/server/services/members/members-api/repositories/ProductRepository.js +662 -0
  87. package/core/server/services/members/members-api/services/GeolocationService.js +23 -0
  88. package/core/server/services/members/members-api/services/MemberBREADService.js +444 -0
  89. package/core/server/services/members/members-api/services/PaymentsService.js +522 -0
  90. package/core/server/services/members/members-api/services/TokenService.js +54 -0
  91. package/core/server/services/milestones/BookshelfMilestoneRepository.js +8 -9
  92. package/core/server/services/milestones/InMemoryMilestoneRepository.js +119 -0
  93. package/core/server/services/milestones/Milestone.js +231 -0
  94. package/core/server/services/milestones/MilestoneCreatedEvent.js +22 -0
  95. package/core/server/services/milestones/MilestonesService.js +327 -0
  96. package/core/server/services/milestones/service.js +2 -2
  97. package/core/server/services/newsletters/index.js +1 -1
  98. package/core/server/services/public-config/config.js +2 -1
  99. package/core/server/services/settings/settings-service.js +1 -1
  100. package/core/server/services/slack-notifications/SlackNotifications.js +1 -1
  101. package/core/server/services/slack-notifications/SlackNotificationsService.js +2 -2
  102. package/core/server/services/staff/StaffService.js +1 -1
  103. package/core/shared/config/defaults.json +3 -0
  104. package/core/shared/config/env/config.testing-mysql.json +3 -0
  105. package/core/shared/config/env/config.testing.json +3 -0
  106. package/core/shared/labs.js +2 -2
  107. package/package.json +63 -63
  108. package/tsconfig.tsbuildinfo +1 -1
  109. package/yarn.lock +306 -70
  110. package/components/tryghost-constants-5.116.2.tgz +0 -0
  111. package/components/tryghost-custom-fonts-5.116.2.tgz +0 -0
  112. package/components/tryghost-custom-theme-settings-service-5.116.2.tgz +0 -0
  113. package/components/tryghost-domain-events-5.116.2.tgz +0 -0
  114. package/components/tryghost-donations-5.116.2.tgz +0 -0
  115. package/components/tryghost-email-addresses-5.116.2.tgz +0 -0
  116. package/components/tryghost-email-suppression-list-5.116.2.tgz +0 -0
  117. package/components/tryghost-html-to-plaintext-5.116.2.tgz +0 -0
  118. package/components/tryghost-i18n-5.116.2.tgz +0 -0
  119. package/components/tryghost-link-replacer-5.116.2.tgz +0 -0
  120. package/components/tryghost-member-events-5.116.2.tgz +0 -0
  121. package/components/tryghost-members-api-5.116.2.tgz +0 -0
  122. package/components/tryghost-milestones-5.116.2.tgz +0 -0
  123. package/components/tryghost-mw-error-handler-5.116.2.tgz +0 -0
  124. package/components/tryghost-mw-vhost-5.116.2.tgz +0 -0
  125. package/components/tryghost-post-revisions-5.116.2.tgz +0 -0
  126. package/components/tryghost-posts-service-5.116.2.tgz +0 -0
  127. package/components/tryghost-prometheus-metrics-5.116.2.tgz +0 -0
  128. package/components/tryghost-security-5.116.2.tgz +0 -0
  129. package/components/tryghost-tiers-5.116.2.tgz +0 -0
  130. package/components/tryghost-webmentions-5.116.2.tgz +0 -0
  131. /package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js.LICENSE.txt → chunk.713.48f120c377bcaffdfddf.js.LICENSE.txt} +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.116%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%22f374d17369%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22bff4438287%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%222c1461dad3%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%22ffcc9f090c%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" />
@@ -57,8 +57,8 @@
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
59
  <script src="assets/vendor-8e3ee8261528bb429cfe78ce79a4a82a.js"></script>
60
- <script src="assets/chunk.713.761d11035fe0bf3e557c.js"></script>
61
- <script src="assets/chunk.524.578de86e5014b911b05a.js"></script>
62
- <script src="assets/ghost-868c537d5c02ca65323d0122596a67ec.js"></script>
60
+ <script src="assets/chunk.713.48f120c377bcaffdfddf.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 = {
@@ -0,0 +1,38 @@
1
+ const {combineNonTransactionalMigrations, createAddColumnMigration} = require('../../utils');
2
+ module.exports = combineNonTransactionalMigrations(
3
+ createAddColumnMigration('users', 'threads', {
4
+ type: 'string',
5
+ maxlength: 191,
6
+ nullable: true
7
+ }),
8
+ createAddColumnMigration('users', 'bluesky', {
9
+ type: 'string',
10
+ maxlength: 191,
11
+ nullable: true
12
+ }),
13
+ createAddColumnMigration('users', 'mastodon', {
14
+ type: 'string',
15
+ maxlength: 191,
16
+ nullable: true
17
+ }),
18
+ createAddColumnMigration('users', 'tiktok', {
19
+ type: 'string',
20
+ maxlength: 191,
21
+ nullable: true
22
+ }),
23
+ createAddColumnMigration('users', 'youtube', {
24
+ type: 'string',
25
+ maxlength: 191,
26
+ nullable: true
27
+ }),
28
+ createAddColumnMigration('users', 'instagram', {
29
+ type: 'string',
30
+ maxlength: 191,
31
+ nullable: true
32
+ }),
33
+ createAddColumnMigration('users', 'linkedin', {
34
+ type: 'string',
35
+ maxlength: 191,
36
+ nullable: true
37
+ })
38
+ );
@@ -131,6 +131,13 @@ module.exports = {
131
131
  location: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max: 150}}},
132
132
  facebook: {type: 'string', maxlength: 2000, nullable: true},
133
133
  twitter: {type: 'string', maxlength: 2000, nullable: true},
134
+ threads: {type: 'string', maxlength: 191, nullable: true},
135
+ bluesky: {type: 'string', maxlength: 191, nullable: true},
136
+ mastodon: {type: 'string', maxlength: 191, nullable: true},
137
+ tiktok: {type: 'string', maxlength: 191, nullable: true},
138
+ youtube: {type: 'string', maxlength: 191, nullable: true},
139
+ instagram: {type: 'string', maxlength: 191, nullable: true},
140
+ linkedin: {type: 'string', maxlength: 191, nullable: true},
134
141
  accessibility: {type: 'text', maxlength: 65535, nullable: true},
135
142
  status: {
136
143
  type: 'string',
@@ -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
 
@@ -15,7 +15,8 @@ function SessionMiddleware({sessionService}) {
15
15
  } else {
16
16
  await sessionService.sendAuthCodeToUser(req, res);
17
17
  throw new errors.NoPermissionError({
18
- code: '2FA_TOKEN_REQUIRED',
18
+ code: sessionService.isVerificationRequired() ? '2FA_TOKEN_REQUIRED' : '2FA_NEW_DEVICE_DETECTED',
19
+ context: 'A 6-digit sign-in verification code has been sent to your email to keep your account safe.',
19
20
  errorType: 'Needs2FAError',
20
21
  message: 'User must verify session to login.'
21
22
  });
@@ -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,
@@ -271,7 +271,7 @@ module.exports = function createSessionService({
271
271
  siteLogo: siteLogo,
272
272
  token: token,
273
273
  deviceDetails: await getDeviceDetails(session.user_agent, session.ip),
274
- is2FARequired: this.isVerificationRequired()
274
+ is2FARequired: isVerificationRequired()
275
275
  });
276
276
 
277
277
  try {
@@ -320,7 +320,7 @@ module.exports = function createSessionService({
320
320
  async function removeUserForSession(req, res) {
321
321
  const session = await getSession(req, res);
322
322
 
323
- if (this.isVerificationRequired()) {
323
+ if (isVerificationRequired()) {
324
324
  session.verified = undefined;
325
325
  }
326
326
 
@@ -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
+ };