diff --git a/apps/admin-x-settings/src/components/settings/advanced/SpamFilters.tsx b/apps/admin-x-settings/src/components/settings/advanced/SpamFilters.tsx index c00b956368f..e285e6aac02 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/SpamFilters.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/SpamFilters.tsx @@ -1,8 +1,9 @@ import React from 'react'; import TopLevelGroup from '../../TopLevelGroup'; import useSettingGroup from '../../../hooks/useSettingGroup'; -import {Separator, SettingGroupContent, TextArea, Toggle, withErrorBoundary} from '@tryghost/admin-x-design-system'; +import {Separator, SettingGroupContent, TextArea, TextField, Toggle, withErrorBoundary} from '@tryghost/admin-x-design-system'; import {getSettingValue, getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {useGlobalData} from '../../providers/GlobalDataProvider'; const SpamFilters: React.FC<{ keywords: string[] }> = ({keywords}) => { const { @@ -21,16 +22,29 @@ const SpamFilters: React.FC<{ keywords: string[] }> = ({keywords}) => { } }); + const {config} = useGlobalData(); + const [initialBlockedEmailDomainsJSON] = getSettingValues(localSettings, ['blocked_email_domains']) as string[]; const initialBlockedEmailDomains = JSON.parse(initialBlockedEmailDomainsJSON || '[]') as string[]; const [blockedEmailDomains, setBlockedEmailDomains] = React.useState(initialBlockedEmailDomains.join('\n')); const [captchaEnabled] = getSettingValues(localSettings, ['captcha_enabled']) as boolean[]; + const [captchaSitekey, captchaSecret] = getSettingValues(localSettings, ['captcha_sitekey', 'captcha_secret']) as string[]; const handleToggleChange = (key: string, e: React.ChangeEvent) => { updateSetting(key, e.target.checked); handleEditingChange(true); }; + const handleSitekeyChange = (e: React.ChangeEvent) => { + updateSetting('captcha_sitekey', e.target.value); + handleEditingChange(true); + }; + + const handleSecretChange = (e: React.ChangeEvent) => { + updateSetting('captcha_secret', e.target.value); + handleEditingChange(true); + }; + const labs = JSON.parse(getSettingValue(localSettings, 'labs') || '{}'); const updateBlockedEmailDomainsSetting = (e: React.ChangeEvent) => { @@ -95,11 +109,31 @@ const SpamFilters: React.FC<{ keywords: string[] }> = ({keywords}) => { gap='gap-0' hint={captchaHint} label='Enable strict signup security' - labelClasses='block text-sm font-medium tracking-normal text-grey-900 w-full mt-[-10px]' + labelClasses='block text-sm font-medium tracking-normal text-grey-900 dark:text-grey-500 w-full mt-[-10px]' onChange={(e) => { handleToggleChange('captcha_enabled', e); }} /> + {/* Sitekey / secret are only modifiable in self-hoster setups */} + {config?.hostSettings?.captcha || (<> + + + + + )} )} diff --git a/ghost/core/core/server/api/endpoints/settings-public.js b/ghost/core/core/server/api/endpoints/settings-public.js index 3a8446576d2..aaedc0bc0d6 100644 --- a/ghost/core/core/server/api/endpoints/settings-public.js +++ b/ghost/core/core/server/api/endpoints/settings-public.js @@ -6,9 +6,15 @@ const labs = require('../../../shared/labs'); const getCaptchaSettings = () => { if (labs.isSet('captcha')) { - return { - captcha_sitekey: config.get('captcha:siteKey') - }; + const siteKey = config.get('hostSettings:captcha:siteKey'); + if (siteKey) { + return { + captcha_sitekey: siteKey + }; + } else { + // Sitekey pulled from settings for self-hosters, no need to override + return {}; + } } else { return { captcha_enabled: false diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js index 0aaf96df2b8..3217942bedf 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js @@ -77,6 +77,8 @@ const EDITABLE_SETTINGS = [ 'heading_font', 'blocked_email_domains', 'captcha_enabled', + 'captcha_sitekey', + 'captcha_secret', 'require_email_mfa' ]; diff --git a/ghost/core/core/server/data/migrations/versions/5.116/2025-03-28-17-58-29-add-captcha-selfhoster-settings.js b/ghost/core/core/server/data/migrations/versions/5.116/2025-03-28-17-58-29-add-captcha-selfhoster-settings.js new file mode 100644 index 00000000000..20593a339f8 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.116/2025-03-28-17-58-29-add-captcha-selfhoster-settings.js @@ -0,0 +1,16 @@ +const {addSetting, combineTransactionalMigrations} = require('../../utils'); + +module.exports = combineTransactionalMigrations([ + addSetting({ + key: 'captcha_sitekey', + value: null, + type: 'string', + group: 'members' + }), + addSetting({ + key: 'captcha_secret', + value: null, + type: 'string', + group: 'members' + }) +]); diff --git a/ghost/core/core/server/data/schema/default-settings/default-settings.json b/ghost/core/core/server/data/schema/default-settings/default-settings.json index 57cd9f91a50..7941bde04a7 100644 --- a/ghost/core/core/server/data/schema/default-settings/default-settings.json +++ b/ghost/core/core/server/data/schema/default-settings/default-settings.json @@ -327,6 +327,14 @@ "isIn": [["true", "false"]] }, "type": "boolean" + }, + "captcha_sitekey": { + "defaultValue": null, + "type": "string" + }, + "captcha_secret": { + "defaultValue": null, + "type": "string" } }, "portal": { diff --git a/ghost/core/core/server/services/members/api.js b/ghost/core/core/server/services/members/api.js index e05b0c5e8c4..3e4db55e4bd 100644 --- a/ghost/core/core/server/services/members/api.js +++ b/ghost/core/core/server/services/members/api.js @@ -244,7 +244,7 @@ function createApiInstance(config) { captchaService: new CaptchaService({ enabled: labsService.isSet('captcha') && sharedConfig.get('captcha:enabled'), scoreThreshold: sharedConfig.get('captcha:scoreThreshold'), - secretKey: sharedConfig.get('captcha:secretKey') + secretKey: sharedConfig.get('hostSettings:captcha:secretKey') || settingsCache.get('captcha_secret') }) }); diff --git a/ghost/core/core/shared/settings-cache/CacheManager.js b/ghost/core/core/shared/settings-cache/CacheManager.js index 1a94e321f78..ba095b81ccd 100644 --- a/ghost/core/core/shared/settings-cache/CacheManager.js +++ b/ghost/core/core/shared/settings-cache/CacheManager.js @@ -60,6 +60,8 @@ const _ = require('lodash'); * @property {string|null} support_email_address - Support email address * @property {string|null} editor_default_email_recipients - Default email recipients for editor * @property {boolean|null} captcha_enabled - Whether captcha is enabled + * @property {string|null} captcha_sitekey - Unique sitekey for hCaptcha + * @property {string|null} captcha_secret - Private key to validate hCaptcha responses * @property {string|null} labs - JSON string of enabled labs features * @property {never} [x] - Prevent accessing undefined properties */