UNPKG

ghost

Version:

The professional publishing platform

184 lines (160 loc) 8.84 kB
/** * Settings Lib * A collection of utilities for handling settings including a cache */ const events = require('../../lib/common/events'); const models = require('../../models'); const labs = require('../../../shared/labs'); const limits = require('../limits'); const config = require('../../../shared/config'); const adapterManager = require('../adapter-manager'); const SettingsCache = require('../../../shared/settings-cache'); const SettingsBREADService = require('./SettingsBREADService'); const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils'); const mail = require('../mail'); const SingleUseTokenProvider = require('../members/SingleUseTokenProvider'); const urlUtils = require('../../../shared/url-utils'); const ObjectId = require('bson-objectid').default; const settingsHelpers = require('../settings-helpers'); const emailAddressService = require('../email-address'); const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000; const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000; const MAGIC_LINK_TOKEN_MAX_USAGE_COUNT = 7; /** * @returns {SettingsBREADService} instance of the PostsService */ const getSettingsBREADServiceInstance = () => { return new SettingsBREADService({ SettingsModel: models.Settings, settingsCache: SettingsCache, labsService: labs, limitsService: limits, mail, singleUseTokenProvider: new SingleUseTokenProvider({ SingleUseTokenModel: models.SingleUseToken, validityPeriod: MAGIC_LINK_TOKEN_VALIDITY, validityPeriodAfterUsage: MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE, maxUsageCount: MAGIC_LINK_TOKEN_MAX_USAGE_COUNT }), urlUtils, emailAddressService: emailAddressService }); }; class CalculatedField { constructor({key, type, group, fn, dependents}) { this.key = key; this.type = type; this.group = group; this.fn = fn; this.dependents = dependents; } getSetting() { return { key: this.key, type: this.type, group: this.group, value: this.fn(), // @TODO: remove this hack id: ObjectId().toHexString(), created_at: new Date().toISOString().replace(/\d{3}Z$/, '000Z'), updated_at: new Date().toISOString().replace(/\d{3}Z$/, '000Z') }; } } module.exports = { /** * Initialize the cache, used in boot and in testing */ async init() { const cacheStore = adapterManager.getAdapter('cache:settings'); const settingsCollection = await models.Settings.populateDefaults(); const settingsOverrides = config.get('hostSettings:settingsOverrides') || {}; SettingsCache.init(events, settingsCollection, this.getCalculatedFields(), cacheStore, settingsOverrides); // Validate site_uuid matches config await this.validateSiteUuid(); }, /** * Restore the cache, used during e2e testing only */ reset() { SettingsCache.reset(events); }, /** * */ getCalculatedFields() { const fields = []; fields.push(new CalculatedField({key: 'members_enabled', type: 'boolean', group: 'members', fn: settingsHelpers.isMembersEnabled.bind(settingsHelpers), dependents: ['members_signup_access']})); fields.push(new CalculatedField({key: 'members_invite_only', type: 'boolean', group: 'members', fn: settingsHelpers.isMembersInviteOnly.bind(settingsHelpers), dependents: ['members_signup_access']})); fields.push(new CalculatedField({key: 'allow_self_signup', type: 'boolean', group: 'members', fn: settingsHelpers.allowSelfSignup.bind(settingsHelpers), dependents: ['members_signup_access', 'portal_plans', 'stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']})); fields.push(new CalculatedField({key: 'paid_members_enabled', type: 'boolean', group: 'members', fn: settingsHelpers.arePaidMembersEnabled.bind(settingsHelpers), dependents: ['members_signup_access', 'stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']})); fields.push(new CalculatedField({key: 'firstpromoter_account', type: 'string', group: 'firstpromoter', fn: settingsHelpers.getFirstpromoterId.bind(settingsHelpers), dependents: ['firstpromoter', 'firstpromoter_id']})); fields.push(new CalculatedField({key: 'donations_enabled', type: 'boolean', group: 'donations', fn: settingsHelpers.areDonationsEnabled.bind(settingsHelpers), dependents: ['stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']})); // E-mail addresses fields.push(new CalculatedField({key: 'default_email_address', type: 'string', group: 'email', fn: settingsHelpers.getDefaultEmailAddress.bind(settingsHelpers), dependents: ['labs']})); fields.push(new CalculatedField({key: 'support_email_address', type: 'string', group: 'email', fn: settingsHelpers.getMembersSupportAddress.bind(settingsHelpers), dependents: ['labs', 'members_support_address']})); // Blocked email domains from member signup, from both config and user settings fields.push(new CalculatedField({key: 'all_blocked_email_domains', type: 'string', group: 'members', fn: settingsHelpers.getAllBlockedEmailDomains.bind(settingsHelpers), dependents: ['blocked_email_domains']})); // Social web (ActivityPub) fields.push(new CalculatedField({key: 'social_web_enabled', type: 'boolean', group: 'social_web', fn: settingsHelpers.isSocialWebEnabled.bind(settingsHelpers), dependents: ['social_web', 'labs']})); // Web analytics fields.push(new CalculatedField({key: 'web_analytics_enabled', type: 'boolean', group: 'analytics', fn: settingsHelpers.isWebAnalyticsEnabled.bind(settingsHelpers), dependents: ['web_analytics', 'labs']})); fields.push(new CalculatedField({key: 'web_analytics_configured', type: 'boolean', group: 'analytics', fn: settingsHelpers.isWebAnalyticsConfigured.bind(settingsHelpers), dependents: ['web_analytics']})); return fields; }, /** * Handles synchronization of routes.yaml hash loaded in the frontend with * the value stored in the settings table. * getRoutesHash is a function to allow keeping "frontend" decoupled from settings * * @param {function} getRoutesHash function fetching currently loaded routes file hash */ async syncRoutesHash(getRoutesHash) { const currentRoutesHash = await getRoutesHash(); if (SettingsCache.get('routes_hash') !== currentRoutesHash) { return await models.Settings.edit([{ key: 'routes_hash', value: currentRoutesHash }], {context: {internal: true}}); } }, /** * Handles email setting synchronization when email has been verified per instance * * @param {boolean} configValue current email verification value from local config */ async syncEmailSettings(configValue) { const isEmailDisabled = SettingsCache.get('email_verification_required'); if (configValue === true && isEmailDisabled) { return await models.Settings.edit([{ key: 'email_verification_required', value: false }], {context: {internal: true}}); } }, /** * Validates that the site_uuid setting matches the configured site_uuid * This is a safeguard to prevent sites from running with the wrong site_uuid * The configured site_uuid is only used once when the site_uuid setting is set in a migration * Exits with an error if they differ */ async validateSiteUuid() { const configSiteUuid = config.get('site_uuid'); const settingSiteUuid = SettingsCache.get('site_uuid'); if (configSiteUuid && settingSiteUuid && configSiteUuid.toLowerCase() !== settingSiteUuid.toLowerCase()) { const logging = require('@tryghost/logging'); const errors = require('@tryghost/errors'); logging.error(`Site UUID mismatch: config has '${configSiteUuid}' but database has '${settingSiteUuid}'`); throw new errors.IncorrectUsageError({ message: 'Site UUID configuration does not match database value', context: 'Ghost will not boot if the configured site_uuid does not match the value in the settings table', help: 'Please check your site_uuid configuration', code: 'SITE_UUID_MISMATCH' }); } }, obfuscatedSetting, isSecretSetting, hideValueIfSecret, getSettingsBREADServiceInstance };