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.
- package/components/{tryghost-api-framework-5.116.2.tgz → tryghost-api-framework-5.118.0.tgz} +0 -0
- package/components/tryghost-constants-5.118.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.118.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.118.0.tgz +0 -0
- package/components/tryghost-domain-events-5.118.0.tgz +0 -0
- package/components/tryghost-donations-5.118.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.118.0.tgz +0 -0
- package/components/{tryghost-email-service-5.116.2.tgz → tryghost-email-service-5.118.0.tgz} +0 -0
- package/components/tryghost-email-suppression-list-5.118.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.118.0.tgz +0 -0
- package/components/tryghost-i18n-5.118.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.116.2.tgz → tryghost-job-manager-5.118.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.118.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.116.2.tgz → tryghost-magic-link-5.118.0.tgz} +0 -0
- package/components/{tryghost-member-attribution-5.116.2.tgz → tryghost-member-attribution-5.118.0.tgz} +0 -0
- package/components/tryghost-member-events-5.118.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.116.2.tgz → tryghost-members-csv-5.118.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.116.2.tgz → tryghost-members-offers-5.118.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.118.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.118.0.tgz +0 -0
- package/components/{tryghost-post-events-5.116.2.tgz → tryghost-post-events-5.118.0.tgz} +0 -0
- package/components/tryghost-post-revisions-5.118.0.tgz +0 -0
- package/components/tryghost-posts-service-5.118.0.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.118.0.tgz +0 -0
- package/components/tryghost-security-5.118.0.tgz +0 -0
- package/components/tryghost-tiers-5.118.0.tgz +0 -0
- package/components/tryghost-webmentions-5.118.0.tgz +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -1
- package/content/themes/casper/author.hbs +23 -2
- package/content/themes/casper/package.json +2 -2
- package/content/themes/casper/partials/icons/bluesky.hbs +3 -0
- package/content/themes/casper/partials/icons/instagram.hbs +5 -0
- package/content/themes/casper/partials/icons/linkedin.hbs +3 -0
- package/content/themes/casper/partials/icons/mastodon.hbs +3 -0
- package/content/themes/casper/partials/icons/threads.hbs +3 -0
- package/content/themes/casper/partials/icons/tiktok.hbs +3 -0
- package/content/themes/casper/partials/icons/twitter.hbs +3 -1
- package/content/themes/casper/partials/icons/youtube.hbs +3 -0
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +7 -12
- package/content/themes/source/author.hbs +24 -3
- package/content/themes/source/package.json +2 -2
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/content/themes/source/partials/icons/bluesky.hbs +3 -0
- package/content/themes/source/partials/icons/instagram.hbs +5 -0
- package/content/themes/source/partials/icons/linkedin.hbs +3 -0
- package/content/themes/source/partials/icons/mastodon.hbs +3 -0
- package/content/themes/source/partials/icons/threads.hbs +3 -0
- package/content/themes/source/partials/icons/tiktok.hbs +3 -0
- package/content/themes/source/partials/icons/youtube.hbs +3 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +31793 -26588
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-550846e0.mjs → CodeEditorView-1143c509.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-f3cb3f4d.mjs → index-19ebc8ad.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-4ce2fcd1.mjs → index-ac104f42.mjs} +2635 -2607
- package/core/built/admin/assets/admin-x-settings/{modals-6bc20529.mjs → modals-994901ee.mjs} +6680 -6165
- package/core/built/admin/assets/{chunk.524.578de86e5014b911b05a.js → chunk.524.5710919eb507b9a81166.js} +8 -8
- package/core/built/admin/assets/{chunk.582.21bf3e37b5d84ac4b58a.js → chunk.582.c8cb99b85cfa13fc7df1.js} +10 -10
- package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js → chunk.713.48f120c377bcaffdfddf.js} +6 -9
- package/core/built/admin/assets/{ghost-868c537d5c02ca65323d0122596a67ec.js → ghost-cd90a28b214ee800a007bb62cd45e6e6.js} +780 -775
- package/core/built/admin/assets/posts/posts.js +11561 -11302
- package/core/built/admin/assets/stats/stats.js +76076 -59355
- package/core/built/admin/index.html +4 -4
- package/core/frontend/helpers/social_url.js +31 -0
- package/core/server/api/endpoints/users.js +7 -0
- package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
- 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
- package/core/server/data/schema/schema.js +7 -0
- package/core/server/services/auth/session/index.js +5 -2
- package/core/server/services/auth/session/middleware.js +2 -1
- package/core/server/services/auth/session/session-service.js +7 -6
- package/core/server/services/members/api.js +2 -2
- package/core/server/services/members/members-api/controllers/MemberController.js +214 -0
- package/core/server/services/members/members-api/controllers/RouterController.js +667 -0
- package/core/server/services/members/members-api/controllers/WellKnownController.js +46 -0
- package/core/server/services/members/members-api/members-api.js +404 -0
- package/core/server/services/members/members-api/repositories/EventRepository.js +984 -0
- package/core/server/services/members/members-api/repositories/MemberRepository.js +1739 -0
- package/core/server/services/members/members-api/repositories/ProductRepository.js +662 -0
- package/core/server/services/members/members-api/services/GeolocationService.js +23 -0
- package/core/server/services/members/members-api/services/MemberBREADService.js +444 -0
- package/core/server/services/members/members-api/services/PaymentsService.js +522 -0
- package/core/server/services/members/members-api/services/TokenService.js +54 -0
- package/core/server/services/milestones/BookshelfMilestoneRepository.js +8 -9
- package/core/server/services/milestones/InMemoryMilestoneRepository.js +119 -0
- package/core/server/services/milestones/Milestone.js +231 -0
- package/core/server/services/milestones/MilestoneCreatedEvent.js +22 -0
- package/core/server/services/milestones/MilestonesService.js +327 -0
- package/core/server/services/milestones/service.js +2 -2
- package/core/server/services/newsletters/index.js +1 -1
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/settings/settings-service.js +1 -1
- package/core/server/services/slack-notifications/SlackNotifications.js +1 -1
- package/core/server/services/slack-notifications/SlackNotificationsService.js +2 -2
- package/core/server/services/staff/StaffService.js +1 -1
- package/core/shared/config/defaults.json +3 -0
- package/core/shared/config/env/config.testing-mysql.json +3 -0
- package/core/shared/config/env/config.testing.json +3 -0
- package/core/shared/labs.js +2 -2
- package/package.json +63 -63
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +306 -70
- package/components/tryghost-constants-5.116.2.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.2.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.116.2.tgz +0 -0
- package/components/tryghost-domain-events-5.116.2.tgz +0 -0
- package/components/tryghost-donations-5.116.2.tgz +0 -0
- package/components/tryghost-email-addresses-5.116.2.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.2.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.2.tgz +0 -0
- package/components/tryghost-i18n-5.116.2.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.2.tgz +0 -0
- package/components/tryghost-member-events-5.116.2.tgz +0 -0
- package/components/tryghost-members-api-5.116.2.tgz +0 -0
- package/components/tryghost-milestones-5.116.2.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.116.2.tgz +0 -0
- package/components/tryghost-mw-vhost-5.116.2.tgz +0 -0
- package/components/tryghost-post-revisions-5.116.2.tgz +0 -0
- package/components/tryghost-posts-service-5.116.2.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.116.2.tgz +0 -0
- package/components/tryghost-security-5.116.2.tgz +0 -0
- package/components/tryghost-tiers-5.116.2.tgz +0 -0
- package/components/tryghost-webmentions-5.116.2.tgz +0 -0
- /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.
|
|
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.
|
|
61
|
-
<script src="assets/chunk.524.
|
|
62
|
-
<script src="assets/ghost-
|
|
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
|
+
};
|
|
@@ -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
|
-
|
|
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 (
|
|
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?://([^/:?#]+)(?:[/:?#]|$)',
|
|
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:
|
|
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 (
|
|
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('
|
|
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 =
|
|
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
|
+
};
|