ghost 6.2.0 → 6.3.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-i18n-6.3.0.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
- package/core/built/admin/assets/admin-x-activitypub/{index-DmCoswaX.mjs → index-C8tyOPu-.mjs} +2 -2
- package/core/built/admin/assets/admin-x-activitypub/{index-lT95Q15h.mjs → index-QqbAPyqT.mjs} +77 -76
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-UxqLGRTu.mjs → CodeEditorView-CHa5Y-LX.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-B5r0jdJS.mjs → index-CGFCkAXn.mjs} +9 -5
- package/core/built/admin/assets/admin-x-settings/{index-Co907MFn.mjs → index-Cg4zMcj4.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-B7j9sxR4.mjs → modals-DH5H9Tgk.mjs} +1036 -1034
- package/core/built/admin/assets/{chunk.397.d5e25bb9baf088f52499.js → chunk.397.a720333cfffc99c47e71.js} +5 -4
- package/core/built/admin/assets/{chunk.524.70595796c7b8c6003a2d.js → chunk.524.aac61953956de04feb53.js} +6 -6
- package/core/built/admin/assets/{chunk.582.d9b970b71da671ac1b7b.js → chunk.582.0a1461429ddbaef85ea9.js} +8 -8
- package/core/built/admin/assets/{ghost-2066304fd0b166e1c16d397dd73ef7b2.js → ghost-1bfab97cb7f550726e894fae6650a808.js} +23 -21
- package/core/built/admin/assets/ghost-8ade80412a20088a4f0a9a1159f0bdba.css +1 -0
- package/core/built/admin/assets/ghost-dark-b128f29fc44b34b6cfb0fc8492266c2a.css +1 -0
- package/core/built/admin/assets/posts/posts.js +30561 -30283
- package/core/built/admin/assets/stats/stats.js +21340 -21270
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/ghost_head.js +2 -1
- package/core/server/api/endpoints/stats.js +37 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +1 -0
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +1 -0
- package/core/server/data/migrations/versions/6.3/2025-10-02-15-13-31-add-members-otc-secret-setting.js +9 -0
- package/core/server/data/schema/default-settings/default-settings.json +4 -0
- package/core/server/models/settings.js +1 -0
- package/core/server/services/donations/DonationBookshelfRepository.js +6 -1
- package/core/server/services/donations/DonationBookshelfRepository.ts +11 -1
- package/core/server/services/donations/DonationPaymentEvent.js +10 -0
- package/core/server/services/donations/DonationPaymentEvent.ts +10 -0
- package/core/server/services/member-attribution/AttributionBuilder.js +55 -10
- package/core/server/services/member-attribution/README.md +101 -0
- package/core/server/services/member-attribution/ReferrerTranslator.js +40 -3
- package/core/server/services/member-attribution/UrlHistory.js +5 -0
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/members-api/controllers/RouterController.js +26 -0
- package/core/server/services/members/members-api/repositories/MemberRepository.js +6 -1
- package/core/server/services/members-events/EventStorage.js +10 -0
- package/core/server/services/stats/ReferrersStatsService.js +143 -0
- package/core/server/services/stats/StatsService.js +17 -0
- package/core/server/services/stripe/StripeAPI.js +7 -2
- package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +6 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/members/app.js +2 -0
- package/core/server/web/shared/middleware/api/spam-prevention.js +76 -0
- package/core/server/web/shared/middleware/brute.js +23 -0
- package/core/shared/config/defaults.json +13 -1
- package/core/shared/config/env/config.testing-browser.json +12 -0
- package/core/shared/config/env/config.testing-mysql.json +12 -0
- package/core/shared/config/env/config.testing.json +12 -0
- package/core/shared/labs.js +1 -0
- package/package.json +5 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +178 -180
- package/components/tryghost-i18n-6.2.0.tgz +0 -0
- package/core/built/admin/assets/ghost-49475952d56ffe89bd47ab9d9c64ada8.css +0 -1
- package/core/built/admin/assets/ghost-dark-27877727751b91f03261d449d74e33b9.css +0 -1
- /package/core/built/admin/assets/{chunk.397.d5e25bb9baf088f52499.js.LICENSE.txt → chunk.397.a720333cfffc99c47e71.js.LICENSE.txt} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const moment = require('moment');
|
|
2
|
+
const errors = require('@tryghost/errors');
|
|
2
3
|
|
|
3
4
|
// Import centralized date utilities
|
|
4
5
|
const {getDateBoundaries, applyDateFilter} = require('./utils/date-utils');
|
|
@@ -455,6 +456,138 @@ class ReferrersStatsService {
|
|
|
455
456
|
meta: {}
|
|
456
457
|
};
|
|
457
458
|
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get UTM growth stats broken down by UTM parameter (fixture data for now)
|
|
462
|
+
* @param {Object} options
|
|
463
|
+
* @param {string} [options.utm_type='utm_source'] - Which UTM field to group by ('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content')
|
|
464
|
+
* @param {string} [options.order='free_members desc'] - Sort order
|
|
465
|
+
* @param {number} [options.limit=50] - Maximum number of results
|
|
466
|
+
* @param {string} [options.post_id] - Optional filter by post ID
|
|
467
|
+
* @returns {Promise<{data: UtmGrowthStat[], meta: {}}>}
|
|
468
|
+
*/
|
|
469
|
+
async getUtmGrowthStats(options = {}) {
|
|
470
|
+
const utmField = options.utm_type || 'utm_source';
|
|
471
|
+
const limit = options.limit || 50;
|
|
472
|
+
const postId = options.post_id;
|
|
473
|
+
|
|
474
|
+
// Validate utm_type is a valid UTM field
|
|
475
|
+
const validUtmFields = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
|
|
476
|
+
if (!validUtmFields.includes(utmField)) {
|
|
477
|
+
throw new errors.BadRequestError({
|
|
478
|
+
message: `Invalid utm_type: ${utmField}. Must be one of: ${validUtmFields.join(', ')}`
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Fixture data; will replace with real data once members service is wired up fully
|
|
483
|
+
let fixtureData = this._getUtmFixtureData(utmField);
|
|
484
|
+
|
|
485
|
+
// If filtering by post, scale down the data
|
|
486
|
+
if (postId) {
|
|
487
|
+
fixtureData = fixtureData.map(item => ({
|
|
488
|
+
...item,
|
|
489
|
+
free_members: Math.floor(item.free_members * 0.3), // 30% of global
|
|
490
|
+
paid_members: Math.floor(item.paid_members * 0.25), // 25% of global
|
|
491
|
+
mrr: Math.floor(item.mrr * 0.25) // 25% of global
|
|
492
|
+
})).filter(item => item.free_members > 0 || item.paid_members > 0); // Only include items with data
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const sortedData = this._sortUtmData(fixtureData, options.order);
|
|
496
|
+
const limitedData = postId ? sortedData : (limit > 0 ? sortedData.slice(0, limit) : sortedData);
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
data: limitedData,
|
|
500
|
+
meta: {}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Generate fixture data for UTM parameters
|
|
506
|
+
* @private
|
|
507
|
+
* @param {string} utmField - The UTM field ('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content')
|
|
508
|
+
* @returns {UtmGrowthStat[]}
|
|
509
|
+
*/
|
|
510
|
+
_getUtmFixtureData(utmField) {
|
|
511
|
+
const fixtures = {
|
|
512
|
+
utm_source: [
|
|
513
|
+
{utm_value: 'google', utm_type: 'utm_source', free_members: 100, paid_members: 20, mrr: 10000},
|
|
514
|
+
{utm_value: 'facebook', utm_type: 'utm_source', free_members: 80, paid_members: 15, mrr: 7500},
|
|
515
|
+
{utm_value: 'twitter', utm_type: 'utm_source', free_members: 60, paid_members: 10, mrr: 5000},
|
|
516
|
+
{utm_value: 'newsletter', utm_type: 'utm_source', free_members: 40, paid_members: 25, mrr: 12500},
|
|
517
|
+
{utm_value: 'linkedin', utm_type: 'utm_source', free_members: 35, paid_members: 12, mrr: 6000},
|
|
518
|
+
{utm_value: 'reddit', utm_type: 'utm_source', free_members: 25, paid_members: 5, mrr: 2500},
|
|
519
|
+
{utm_value: 'youtube', utm_type: 'utm_source', free_members: 20, paid_members: 8, mrr: 4000},
|
|
520
|
+
{utm_value: 'instagram', utm_type: 'utm_source', free_members: 18, paid_members: 6, mrr: 3000}
|
|
521
|
+
],
|
|
522
|
+
utm_medium: [
|
|
523
|
+
{utm_value: 'organic', utm_type: 'utm_medium', free_members: 150, paid_members: 30, mrr: 15000},
|
|
524
|
+
{utm_value: 'cpc', utm_type: 'utm_medium', free_members: 90, paid_members: 20, mrr: 10000},
|
|
525
|
+
{utm_value: 'email', utm_type: 'utm_medium', free_members: 70, paid_members: 20, mrr: 10000},
|
|
526
|
+
{utm_value: 'social', utm_type: 'utm_medium', free_members: 30, paid_members: 10, mrr: 5000},
|
|
527
|
+
{utm_value: 'referral', utm_type: 'utm_medium', free_members: 25, paid_members: 8, mrr: 4000},
|
|
528
|
+
{utm_value: 'display', utm_type: 'utm_medium', free_members: 15, paid_members: 3, mrr: 1500}
|
|
529
|
+
],
|
|
530
|
+
utm_campaign: [
|
|
531
|
+
{utm_value: 'spring-sale', utm_type: 'utm_campaign', free_members: 120, paid_members: 35, mrr: 17500},
|
|
532
|
+
{utm_value: 'product-launch', utm_type: 'utm_campaign', free_members: 80, paid_members: 20, mrr: 10000},
|
|
533
|
+
{utm_value: 'webinar-series', utm_type: 'utm_campaign', free_members: 60, paid_members: 15, mrr: 7500},
|
|
534
|
+
{utm_value: 'holiday-promo', utm_type: 'utm_campaign', free_members: 45, paid_members: 18, mrr: 9000},
|
|
535
|
+
{utm_value: 'content-upgrade', utm_type: 'utm_campaign', free_members: 30, paid_members: 8, mrr: 4000},
|
|
536
|
+
{utm_value: 'partner-collab', utm_type: 'utm_campaign', free_members: 25, paid_members: 12, mrr: 6000}
|
|
537
|
+
],
|
|
538
|
+
utm_term: [
|
|
539
|
+
{utm_value: 'best-email-marketing', utm_type: 'utm_term', free_members: 85, paid_members: 22, mrr: 11000},
|
|
540
|
+
{utm_value: 'ghost-cms', utm_type: 'utm_term', free_members: 70, paid_members: 18, mrr: 9000},
|
|
541
|
+
{utm_value: 'newsletter-platform', utm_type: 'utm_term', free_members: 55, paid_members: 15, mrr: 7500},
|
|
542
|
+
{utm_value: 'content-management', utm_type: 'utm_term', free_members: 40, paid_members: 10, mrr: 5000},
|
|
543
|
+
{utm_value: 'publishing-platform', utm_type: 'utm_term', free_members: 30, paid_members: 8, mrr: 4000},
|
|
544
|
+
{utm_value: 'membership-software', utm_type: 'utm_term', free_members: 20, paid_members: 5, mrr: 2500}
|
|
545
|
+
],
|
|
546
|
+
utm_content: [
|
|
547
|
+
{utm_value: 'hero-cta', utm_type: 'utm_content', free_members: 95, paid_members: 25, mrr: 12500},
|
|
548
|
+
{utm_value: 'sidebar-banner', utm_type: 'utm_content', free_members: 75, paid_members: 18, mrr: 9000},
|
|
549
|
+
{utm_value: 'footer-link', utm_type: 'utm_content', free_members: 50, paid_members: 12, mrr: 6000},
|
|
550
|
+
{utm_value: 'email-button', utm_type: 'utm_content', free_members: 45, paid_members: 15, mrr: 7500},
|
|
551
|
+
{utm_value: 'popup-form', utm_type: 'utm_content', free_members: 35, paid_members: 8, mrr: 4000},
|
|
552
|
+
{utm_value: 'text-link', utm_type: 'utm_content', free_members: 25, paid_members: 6, mrr: 3000}
|
|
553
|
+
]
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
return fixtures[utmField] || fixtures.utm_source;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Sort UTM data by the specified order
|
|
561
|
+
* @private
|
|
562
|
+
* @param {UtmGrowthStat[]} data
|
|
563
|
+
* @param {string} [order='free_members desc']
|
|
564
|
+
* @returns {UtmGrowthStat[]}
|
|
565
|
+
*/
|
|
566
|
+
_sortUtmData(data, order = 'free_members desc') {
|
|
567
|
+
const [field, direction] = order.split(' ');
|
|
568
|
+
const validFields = ['free_members', 'paid_members', 'mrr', 'utm_value'];
|
|
569
|
+
|
|
570
|
+
if (!validFields.includes(field)) {
|
|
571
|
+
return data;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return [...data].sort((a, b) => {
|
|
575
|
+
let valueA = a[field];
|
|
576
|
+
let valueB = b[field];
|
|
577
|
+
|
|
578
|
+
// Handle string sorting for utm_value
|
|
579
|
+
if (field === 'utm_value') {
|
|
580
|
+
valueA = String(valueA).toLowerCase();
|
|
581
|
+
valueB = String(valueB).toLowerCase();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (direction === 'asc') {
|
|
585
|
+
return valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
|
586
|
+
}
|
|
587
|
+
// Default to desc
|
|
588
|
+
return valueA < valueB ? 1 : valueA > valueB ? -1 : 0;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
458
591
|
}
|
|
459
592
|
|
|
460
593
|
module.exports = ReferrersStatsService;
|
|
@@ -507,3 +640,13 @@ module.exports.normalizeSource = normalizeSource;
|
|
|
507
640
|
* @property {number} mrr Total MRR from this source (in cents)
|
|
508
641
|
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
|
|
509
642
|
**/
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @typedef {object} UtmGrowthStat
|
|
646
|
+
* @type {Object}
|
|
647
|
+
* @property {string} utm_value - The UTM parameter value (e.g., 'google', 'facebook')
|
|
648
|
+
* @property {string} utm_type - The UTM parameter type ('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content')
|
|
649
|
+
* @property {number} free_members - Count of free member signups
|
|
650
|
+
* @property {number} paid_members - Count of paid member conversions
|
|
651
|
+
* @property {number} mrr - Total MRR from this UTM parameter (in cents)
|
|
652
|
+
**/
|
|
@@ -244,6 +244,23 @@ class StatsService {
|
|
|
244
244
|
return this.referrers.getTopSourcesWithRange(options);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Get UTM growth stats broken down by UTM field (fixture data)
|
|
249
|
+
* Can be filtered by post using post_id parameter
|
|
250
|
+
* @param {Object} options
|
|
251
|
+
* @param {string} [options.utm_type='utm_source'] - Which UTM field to group by ('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content')
|
|
252
|
+
* @param {string} [options.order='free_members desc'] - Sort order
|
|
253
|
+
* @param {number} [options.limit=50] - Maximum number of results (ignored when filtering by post)
|
|
254
|
+
* @param {string} [options.date_from] - Start date in YYYY-MM-DD format (not yet implemented for fixtures)
|
|
255
|
+
* @param {string} [options.date_to] - End date in YYYY-MM-DD format (not yet implemented for fixtures)
|
|
256
|
+
* @param {string} [options.timezone] - Timezone to use for date interpretation (not yet implemented for fixtures)
|
|
257
|
+
* @param {string} [options.post_id] - Optional filter by post ID
|
|
258
|
+
* @returns {Promise<{data: import('./ReferrersStatsService').UtmGrowthStat[], meta: {}}>}
|
|
259
|
+
*/
|
|
260
|
+
async getUtmGrowthStats(options = {}) {
|
|
261
|
+
return this.referrers.getUtmGrowthStats(options);
|
|
262
|
+
}
|
|
263
|
+
|
|
247
264
|
/**
|
|
248
265
|
* @param {object} deps
|
|
249
266
|
*
|
|
@@ -526,13 +526,18 @@ module.exports = class StripeAPI {
|
|
|
526
526
|
items: [{
|
|
527
527
|
plan: priceId
|
|
528
528
|
}],
|
|
529
|
-
metadata: {
|
|
529
|
+
metadata: {
|
|
530
530
|
attribution_id: metadata?.attribution_id,
|
|
531
531
|
attribution_url: metadata?.attribution_url,
|
|
532
532
|
attribution_type: metadata?.attribution_type,
|
|
533
533
|
referrer_source: metadata?.referrer_source,
|
|
534
534
|
referrer_medium: metadata?.referrer_medium,
|
|
535
|
-
referrer_url: metadata?.referrer_url
|
|
535
|
+
referrer_url: metadata?.referrer_url,
|
|
536
|
+
utm_source: metadata?.utm_source,
|
|
537
|
+
utm_medium: metadata?.utm_medium,
|
|
538
|
+
utm_campaign: metadata?.utm_campaign,
|
|
539
|
+
utm_term: metadata?.utm_term,
|
|
540
|
+
utm_content: metadata?.utm_content
|
|
536
541
|
}
|
|
537
542
|
};
|
|
538
543
|
|
|
@@ -75,7 +75,12 @@ module.exports = class CheckoutSessionEventService {
|
|
|
75
75
|
attributionType: session.metadata?.attribution_type ?? null,
|
|
76
76
|
referrerSource: session.metadata?.referrer_source ?? null,
|
|
77
77
|
referrerMedium: session.metadata?.referrer_medium ?? null,
|
|
78
|
-
referrerUrl: session.metadata?.referrer_url ?? null
|
|
78
|
+
referrerUrl: session.metadata?.referrer_url ?? null,
|
|
79
|
+
utmSource: session.metadata?.utm_source ?? null,
|
|
80
|
+
utmMedium: session.metadata?.utm_medium ?? null,
|
|
81
|
+
utmCampaign: session.metadata?.utm_campaign ?? null,
|
|
82
|
+
utmTerm: session.metadata?.utm_term ?? null,
|
|
83
|
+
utmContent: session.metadata?.utm_content ?? null
|
|
79
84
|
});
|
|
80
85
|
|
|
81
86
|
const donationRepository = this.deps.donationRepository;
|
|
@@ -165,6 +165,7 @@ module.exports = function apiRoutes() {
|
|
|
165
165
|
router.get('/stats/posts/:id/top-referrers', mw.authAdminApi, http(api.stats.postReferrersAlpha));
|
|
166
166
|
router.get('/stats/posts/:id/growth', mw.authAdminApi, http(api.stats.postGrowthStats));
|
|
167
167
|
router.get('/stats/top-sources-growth', mw.authAdminApi, http(api.stats.topSourcesGrowth));
|
|
168
|
+
router.get('/stats/utm-growth', mw.authAdminApi, http(api.stats.utmGrowth));
|
|
168
169
|
router.post('/stats/posts-visitor-counts', mw.authAdminApi, http(api.stats.postsVisitorCounts));
|
|
169
170
|
router.post('/stats/posts-member-counts', mw.authAdminApi, http(api.stats.postsMemberCounts));
|
|
170
171
|
|
|
@@ -89,6 +89,8 @@ module.exports = function setupMembersApp() {
|
|
|
89
89
|
'/api/verify-otc',
|
|
90
90
|
bodyParser.json(),
|
|
91
91
|
middleware.verifyIntegrityToken,
|
|
92
|
+
shared.middleware.brute.otcVerificationEnumeration,
|
|
93
|
+
shared.middleware.brute.otcVerification,
|
|
92
94
|
// NOTE: this is wrapped in a function to ensure we always go via the getter
|
|
93
95
|
function lazyVerifyOTCMw(req, res, next) {
|
|
94
96
|
return membersService.api.middleware.verifyOTC(req, res, next);
|
|
@@ -21,6 +21,10 @@ const messages = {
|
|
|
21
21
|
context: 'Too many login attempts.'
|
|
22
22
|
},
|
|
23
23
|
tooManyAttempts: 'Too many attempts.',
|
|
24
|
+
tooManyOTCVerificationAttempts: {
|
|
25
|
+
error: 'Too many attempts for this verification code.',
|
|
26
|
+
context: 'Too many verification code attempts.'
|
|
27
|
+
},
|
|
24
28
|
webmentionsBlock: 'Too many mention attempts',
|
|
25
29
|
emailPreviewBlock: 'Only 10 test emails can be sent per hour'
|
|
26
30
|
};
|
|
@@ -35,6 +39,8 @@ let spamMemberLogin = spam.member_login || {};
|
|
|
35
39
|
let spamContentApiKey = spam.content_api_key || {};
|
|
36
40
|
let spamWebmentionsBlock = spam.webmentions_block || {};
|
|
37
41
|
let spamEmailPreviewBlock = spam.email_preview_block || {};
|
|
42
|
+
let spamOtcVerificationEnumeration = spam.otc_verification_enumeration || {};
|
|
43
|
+
let spamOtcVerification = spam.otc_verification || {};
|
|
38
44
|
|
|
39
45
|
let store;
|
|
40
46
|
let memoryStore;
|
|
@@ -50,6 +56,8 @@ let sendVerificationCodeInstance;
|
|
|
50
56
|
let userVerificationInstance;
|
|
51
57
|
let contentApiKeyInstance;
|
|
52
58
|
let emailPreviewBlockInstance;
|
|
59
|
+
let otcVerificationEnumerationInstance;
|
|
60
|
+
let otcVerificationInstance;
|
|
53
61
|
|
|
54
62
|
const spamConfigKeys = ['freeRetries', 'minWait', 'maxWait', 'lifetime'];
|
|
55
63
|
|
|
@@ -248,6 +256,68 @@ const membersAuthEnumeration = () => {
|
|
|
248
256
|
return membersAuthEnumerationInstance;
|
|
249
257
|
};
|
|
250
258
|
|
|
259
|
+
const otcVerificationEnumeration = () => {
|
|
260
|
+
const ExpressBrute = require('express-brute');
|
|
261
|
+
const BruteKnex = require('brute-knex');
|
|
262
|
+
const db = require('../../../../data/db');
|
|
263
|
+
|
|
264
|
+
store = store || new BruteKnex({
|
|
265
|
+
tablename: 'brute',
|
|
266
|
+
createTable: false,
|
|
267
|
+
knex: db.knex
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!otcVerificationEnumerationInstance) {
|
|
271
|
+
otcVerificationEnumerationInstance = new ExpressBrute(store,
|
|
272
|
+
extend({
|
|
273
|
+
attachResetToRequest: false,
|
|
274
|
+
failCallback(req, res, next, nextValidRequestDate) {
|
|
275
|
+
return next(new errors.TooManyRequestsError({
|
|
276
|
+
message: `Too many verification attempts across multiple codes, try again in ${moment(nextValidRequestDate).fromNow(true)}`,
|
|
277
|
+
context: tpl(messages.tooManyOTCVerificationAttempts.context),
|
|
278
|
+
help: tpl(messages.tooManyOTCVerificationAttempts.context),
|
|
279
|
+
code: 'OTC_TOTAL_ATTEMPTS_RATE_LIMITED'
|
|
280
|
+
}));
|
|
281
|
+
},
|
|
282
|
+
handleStoreError: handleStoreError
|
|
283
|
+
}, pick(spamOtcVerificationEnumeration, spamConfigKeys))
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return otcVerificationEnumerationInstance;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const otcVerification = () => {
|
|
291
|
+
const ExpressBrute = require('express-brute');
|
|
292
|
+
const BruteKnex = require('brute-knex');
|
|
293
|
+
const db = require('../../../../data/db');
|
|
294
|
+
|
|
295
|
+
store = store || new BruteKnex({
|
|
296
|
+
tablename: 'brute',
|
|
297
|
+
createTable: false,
|
|
298
|
+
knex: db.knex
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!otcVerificationInstance) {
|
|
302
|
+
otcVerificationInstance = new ExpressBrute(store,
|
|
303
|
+
extend({
|
|
304
|
+
attachResetToRequest: false,
|
|
305
|
+
failCallback(req, res, next, nextValidRequestDate) {
|
|
306
|
+
return next(new errors.TooManyRequestsError({
|
|
307
|
+
message: `Too many attempts for this verification code, try again in ${moment(nextValidRequestDate).fromNow(true)}`,
|
|
308
|
+
context: tpl(messages.tooManyOTCVerificationAttempts.context),
|
|
309
|
+
help: tpl(messages.tooManyOTCVerificationAttempts.context),
|
|
310
|
+
code: 'OTC_CODE_ATTEMPTS_RATE_LIMITED'
|
|
311
|
+
}));
|
|
312
|
+
},
|
|
313
|
+
handleStoreError: handleStoreError
|
|
314
|
+
}, pick(spamOtcVerification, spamConfigKeys))
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return otcVerificationInstance;
|
|
319
|
+
};
|
|
320
|
+
|
|
251
321
|
// Stops login attempts for a user+IP pair with an increasing time period starting from 10 minutes
|
|
252
322
|
// and rising to a week in a fibonnaci sequence
|
|
253
323
|
// The user+IP count is reset when on successful login
|
|
@@ -432,6 +502,8 @@ module.exports = {
|
|
|
432
502
|
userVerification: userVerification,
|
|
433
503
|
membersAuth: membersAuth,
|
|
434
504
|
membersAuthEnumeration: membersAuthEnumeration,
|
|
505
|
+
otcVerification: otcVerification,
|
|
506
|
+
otcVerificationEnumeration: otcVerificationEnumeration,
|
|
435
507
|
userReset: userReset,
|
|
436
508
|
privateBlog: privateBlog,
|
|
437
509
|
contentApiKey: contentApiKey,
|
|
@@ -450,6 +522,8 @@ module.exports = {
|
|
|
450
522
|
sendVerificationCodeInstance = undefined;
|
|
451
523
|
userVerificationInstance = undefined;
|
|
452
524
|
contentApiKeyInstance = undefined;
|
|
525
|
+
otcVerificationEnumerationInstance = undefined;
|
|
526
|
+
otcVerificationInstance = undefined;
|
|
453
527
|
|
|
454
528
|
spam = config.get('spam') || {};
|
|
455
529
|
spamPrivateBlock = spam.private_block || {};
|
|
@@ -461,5 +535,7 @@ module.exports = {
|
|
|
461
535
|
spamUserVerification = spam.user_verification || {};
|
|
462
536
|
spamMemberLogin = spam.member_login || {};
|
|
463
537
|
spamContentApiKey = spam.content_api_key || {};
|
|
538
|
+
spamOtcVerificationEnumeration = spam.otc_verification_enumeration || {};
|
|
539
|
+
spamOtcVerification = spam.otc_verification || {};
|
|
464
540
|
}
|
|
465
541
|
};
|
|
@@ -128,6 +128,29 @@ module.exports = {
|
|
|
128
128
|
return spamPrevention.membersAuthEnumeration().prevent(req, res, next);
|
|
129
129
|
},
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Block too many OTC verification attempts from same IP (blocks user enumeration)
|
|
133
|
+
*/
|
|
134
|
+
otcVerificationEnumeration(req, res, next) {
|
|
135
|
+
return spamPrevention.otcVerificationEnumeration().prevent(req, res, next);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Block too many attempts for the same otcRef
|
|
140
|
+
*/
|
|
141
|
+
otcVerification(req, res, next) {
|
|
142
|
+
return spamPrevention.otcVerification().getMiddleware({
|
|
143
|
+
// ignoring IP here blocks rotating ip attacks, only one IP should receive an otcRef so it shouldn't cause false positives
|
|
144
|
+
ignoreIP: true,
|
|
145
|
+
key(_req, _res, _next) {
|
|
146
|
+
if (_req.body.otcRef) {
|
|
147
|
+
return _next(`${_req.body.otcRef}otc_verification`);
|
|
148
|
+
}
|
|
149
|
+
return _next();
|
|
150
|
+
}
|
|
151
|
+
})(req, res, next);
|
|
152
|
+
},
|
|
153
|
+
|
|
131
154
|
/**
|
|
132
155
|
* Blocks webmention spam
|
|
133
156
|
*/
|
|
@@ -130,6 +130,18 @@
|
|
|
130
130
|
"lifetime": 3600,
|
|
131
131
|
"freeRetries": 10
|
|
132
132
|
},
|
|
133
|
+
"otc_verification_enumeration": {
|
|
134
|
+
"minWait": 600000,
|
|
135
|
+
"maxWait": 43200000,
|
|
136
|
+
"lifetime": 43200,
|
|
137
|
+
"freeRetries": 8
|
|
138
|
+
},
|
|
139
|
+
"otc_verification": {
|
|
140
|
+
"minWait": 3600000,
|
|
141
|
+
"maxWait": 3600000,
|
|
142
|
+
"lifetime": 3600,
|
|
143
|
+
"freeRetries": 4
|
|
144
|
+
},
|
|
133
145
|
"blocked_email_domains": []
|
|
134
146
|
},
|
|
135
147
|
"caching": {
|
|
@@ -212,7 +224,7 @@
|
|
|
212
224
|
},
|
|
213
225
|
"portal": {
|
|
214
226
|
"url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
|
|
215
|
-
"version": "2.
|
|
227
|
+
"version": "2.55"
|
|
216
228
|
},
|
|
217
229
|
"sodoSearch": {
|
|
218
230
|
"url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
|
|
@@ -70,6 +70,18 @@
|
|
|
70
70
|
"maxWait": 360000,
|
|
71
71
|
"lifetime": 3600,
|
|
72
72
|
"freeRetries": 10
|
|
73
|
+
},
|
|
74
|
+
"otc_verification_enumeration": {
|
|
75
|
+
"minWait": 600000,
|
|
76
|
+
"maxWait": 43200000,
|
|
77
|
+
"lifetime": 43200,
|
|
78
|
+
"freeRetries": 8
|
|
79
|
+
},
|
|
80
|
+
"otc_verification": {
|
|
81
|
+
"minWait": 300000,
|
|
82
|
+
"maxWait": 3600000,
|
|
83
|
+
"lifetime": 3600,
|
|
84
|
+
"freeRetries": 4
|
|
73
85
|
}
|
|
74
86
|
},
|
|
75
87
|
"privacy": {
|
|
@@ -74,6 +74,18 @@
|
|
|
74
74
|
"maxWait": 360000,
|
|
75
75
|
"lifetime": 3600,
|
|
76
76
|
"freeRetries": 10
|
|
77
|
+
},
|
|
78
|
+
"otc_verification_enumeration": {
|
|
79
|
+
"minWait": 600000,
|
|
80
|
+
"maxWait": 43200000,
|
|
81
|
+
"lifetime": 43200,
|
|
82
|
+
"freeRetries": 8
|
|
83
|
+
},
|
|
84
|
+
"otc_verification": {
|
|
85
|
+
"minWait": 300000,
|
|
86
|
+
"maxWait": 3600000,
|
|
87
|
+
"lifetime": 3600,
|
|
88
|
+
"freeRetries": 4
|
|
77
89
|
}
|
|
78
90
|
},
|
|
79
91
|
"privacy": {
|
|
@@ -70,6 +70,18 @@
|
|
|
70
70
|
"maxWait": 360000,
|
|
71
71
|
"lifetime": 3600,
|
|
72
72
|
"freeRetries": 10
|
|
73
|
+
},
|
|
74
|
+
"otc_verification_enumeration": {
|
|
75
|
+
"minWait": 600000,
|
|
76
|
+
"maxWait": 43200000,
|
|
77
|
+
"lifetime": 43200,
|
|
78
|
+
"freeRetries": 8
|
|
79
|
+
},
|
|
80
|
+
"otc_verification": {
|
|
81
|
+
"minWait": 300000,
|
|
82
|
+
"maxWait": 3600000,
|
|
83
|
+
"lifetime": 3600,
|
|
84
|
+
"freeRetries": 4
|
|
73
85
|
}
|
|
74
86
|
},
|
|
75
87
|
"privacy": {
|
package/core/shared/labs.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "The professional publishing platform",
|
|
5
5
|
"author": "Ghost Foundation",
|
|
6
6
|
"homepage": "https://ghost.org",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@tryghost/helpers": "1.1.97",
|
|
87
87
|
"@tryghost/html-to-plaintext": "1.0.4",
|
|
88
88
|
"@tryghost/http-cache-utils": "0.1.20",
|
|
89
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-6.
|
|
89
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-6.3.0.tgz",
|
|
90
90
|
"@tryghost/image-transform": "1.4.6",
|
|
91
91
|
"@tryghost/job-manager": "1.0.3",
|
|
92
92
|
"@tryghost/kg-card-factory": "5.1.2",
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
"moment": "2.24.0",
|
|
197
197
|
"moment-timezone": "0.5.45",
|
|
198
198
|
"multer": "2.0.2",
|
|
199
|
-
"mysql2": "3.15.
|
|
199
|
+
"mysql2": "3.15.2",
|
|
200
200
|
"nconf": "0.13.0",
|
|
201
201
|
"node-fetch": "2.7.0",
|
|
202
202
|
"node-jose": "2.2.0",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
"probe-image-size": "7.2.3",
|
|
209
209
|
"rss": "1.2.2",
|
|
210
210
|
"sanitize-html": "2.17.0",
|
|
211
|
-
"semver": "7.7.
|
|
211
|
+
"semver": "7.7.3",
|
|
212
212
|
"simple-dom": "1.4.0",
|
|
213
213
|
"stoppable": "1.1.0",
|
|
214
214
|
"stripe": "8.222.0",
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
"jackspeak": "2.3.6",
|
|
274
274
|
"moment": "2.24.0",
|
|
275
275
|
"moment-timezone": "0.5.45",
|
|
276
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-6.
|
|
276
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-6.3.0.tgz"
|
|
277
277
|
},
|
|
278
278
|
"nx": {
|
|
279
279
|
"targets": {
|