mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-22 18:31:57 +03:00
bd597db829
- This is part of the quest to separate the frontend and server & get rid of all the places where there are cross-requires - At the moment the settings cache is one big shared cache used by the frontend and server liberally - This change doesn't really solve the fundamental problems, as we still depend on events, and requires from inside frontend - However it allows us to control the misuse slightly better by getting rid of restricted requires and turning on that eslint ruleset
355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
const Promise = require('bluebird');
|
|
const _ = require('lodash');
|
|
const validator = require('@tryghost/validator');
|
|
const models = require('../../models');
|
|
const routeSettings = require('../../services/route-settings');
|
|
const frontendSettings = require('../../../frontend/services/settings');
|
|
const i18n = require('../../../shared/i18n');
|
|
const {BadRequestError, NoPermissionError, NotFoundError} = require('@tryghost/errors');
|
|
const settingsService = require('../../services/settings');
|
|
const settingsCache = require('../../../shared/settings-cache');
|
|
const membersService = require('../../services/members');
|
|
const ghostBookshelf = require('../../models/base');
|
|
|
|
module.exports = {
|
|
docName: 'settings',
|
|
|
|
browse: {
|
|
options: ['group'],
|
|
permissions: true,
|
|
query(frame) {
|
|
let settings = settingsCache.getAll();
|
|
|
|
// CASE: no context passed (functional call)
|
|
if (!frame.options.context) {
|
|
return Promise.resolve(settings.filter((setting) => {
|
|
return setting.group === 'site';
|
|
}));
|
|
}
|
|
|
|
if (!frame.options.context.internal) {
|
|
// CASE: omit core settings unless internal request
|
|
settings = _.filter(settings, (setting) => {
|
|
const isCore = setting.group === 'core';
|
|
return !isCore;
|
|
});
|
|
// CASE: omit secret settings unless internal request
|
|
settings = settings.map(settingsService.hideValueIfSecret);
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
},
|
|
|
|
read: {
|
|
options: ['key'],
|
|
validation: {
|
|
options: {
|
|
key: {
|
|
required: true
|
|
}
|
|
}
|
|
},
|
|
permissions: {
|
|
identifier(frame) {
|
|
return frame.options.key;
|
|
}
|
|
},
|
|
query(frame) {
|
|
let setting;
|
|
if (frame.options.key === 'slack') {
|
|
const slackURL = settingsCache.get('slack_url', {resolve: false});
|
|
const slackUsername = settingsCache.get('slack_username', {resolve: false});
|
|
|
|
setting = slackURL || slackUsername;
|
|
setting.key = 'slack';
|
|
setting.value = [{
|
|
url: slackURL && slackURL.value,
|
|
username: slackUsername && slackUsername.value
|
|
}];
|
|
} else {
|
|
setting = settingsCache.get(frame.options.key, {resolve: false});
|
|
}
|
|
|
|
if (!setting) {
|
|
return Promise.reject(new NotFoundError({
|
|
message: i18n.t('errors.api.settings.problemFindingSetting', {
|
|
key: frame.options.key
|
|
})
|
|
}));
|
|
}
|
|
|
|
// @TODO: handle in settings model permissible fn
|
|
if (setting.group === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
|
return Promise.reject(new NoPermissionError({
|
|
message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
|
|
}));
|
|
}
|
|
|
|
setting = settingsService.hideValueIfSecret(setting);
|
|
|
|
return {
|
|
[frame.options.key]: setting
|
|
};
|
|
}
|
|
},
|
|
|
|
validateMembersEmailUpdate: {
|
|
options: [
|
|
'token',
|
|
'action'
|
|
],
|
|
permissions: false,
|
|
validation: {
|
|
options: {
|
|
token: {
|
|
required: true
|
|
},
|
|
action: {
|
|
values: ['fromaddressupdate', 'supportaddressupdate']
|
|
}
|
|
}
|
|
},
|
|
async query(frame) {
|
|
// This is something you have to do if you want to use the "framework" with access to the raw req/res
|
|
frame.response = async function (req, res) {
|
|
try {
|
|
const {token, action} = frame.options;
|
|
const updatedEmailAddress = await membersService.settings.getEmailFromToken({token});
|
|
const actionToKeyMapping = {
|
|
fromAddressUpdate: 'members_from_address',
|
|
supportAddressUpdate: 'members_support_address'
|
|
};
|
|
if (updatedEmailAddress) {
|
|
return models.Settings.edit({
|
|
key: actionToKeyMapping[action],
|
|
value: updatedEmailAddress
|
|
}).then(() => {
|
|
// Redirect to Ghost-Admin settings page
|
|
const adminLink = membersService.settings.getAdminRedirectLink({type: action});
|
|
res.redirect(adminLink);
|
|
});
|
|
} else {
|
|
return Promise.reject(new BadRequestError({
|
|
message: 'Invalid token!'
|
|
}));
|
|
}
|
|
} catch (err) {
|
|
return Promise.reject(new BadRequestError({
|
|
err,
|
|
message: 'Invalid token!'
|
|
}));
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
updateMembersEmail: {
|
|
permissions: {
|
|
method: 'edit'
|
|
},
|
|
data: [
|
|
'email',
|
|
'type'
|
|
],
|
|
async query(frame) {
|
|
const {email, type} = frame.data;
|
|
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
|
throw new BadRequestError({
|
|
message: i18n.t('errors.api.settings.invalidEmailReceived')
|
|
});
|
|
}
|
|
|
|
if (!type || !['fromAddressUpdate', 'supportAddressUpdate'].includes(type)) {
|
|
throw new BadRequestError({
|
|
message: 'Invalid email type recieved'
|
|
});
|
|
}
|
|
try {
|
|
// Send magic link to update fromAddress
|
|
await membersService.settings.sendEmailAddressUpdateMagicLink({
|
|
email,
|
|
type
|
|
});
|
|
} catch (err) {
|
|
throw new BadRequestError({
|
|
err,
|
|
message: i18n.t('errors.mail.failedSendingEmail.error')
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
disconnectStripeConnectIntegration: {
|
|
permissions: {
|
|
method: 'edit'
|
|
},
|
|
async query(frame) {
|
|
const hasActiveStripeSubscriptions = await membersService.api.hasActiveStripeSubscriptions();
|
|
if (hasActiveStripeSubscriptions) {
|
|
throw new BadRequestError({
|
|
message: 'Cannot disconnect Stripe whilst you have active subscriptions.'
|
|
});
|
|
}
|
|
|
|
/** Delete all Stripe data from DB */
|
|
await ghostBookshelf.knex.raw(`
|
|
UPDATE products SET monthly_price_id = null, yearly_price_id = null
|
|
`);
|
|
await ghostBookshelf.knex.raw(`
|
|
DELETE FROM stripe_prices
|
|
`);
|
|
await ghostBookshelf.knex.raw(`
|
|
DELETE FROM stripe_products
|
|
`);
|
|
await ghostBookshelf.knex.raw(`
|
|
DELETE FROM members_stripe_customers
|
|
`);
|
|
|
|
return models.Settings.edit([{
|
|
key: 'stripe_connect_publishable_key',
|
|
value: null
|
|
}, {
|
|
key: 'stripe_connect_secret_key',
|
|
value: null
|
|
}, {
|
|
key: 'stripe_connect_livemode',
|
|
value: null
|
|
}, {
|
|
key: 'stripe_connect_display_name',
|
|
value: null
|
|
}, {
|
|
key: 'stripe_connect_account_id',
|
|
value: null
|
|
}], frame.options);
|
|
}
|
|
},
|
|
|
|
edit: {
|
|
headers: {
|
|
cacheInvalidate: true
|
|
},
|
|
permissions: {
|
|
unsafeAttrsObject(frame) {
|
|
return _.find(frame.data.settings, {key: 'labs'});
|
|
},
|
|
async before(frame) {
|
|
if (frame.options.context && frame.options.context.internal) {
|
|
return;
|
|
}
|
|
|
|
const firstCoreSetting = frame.data.settings.find(setting => setting.group === 'core');
|
|
if (firstCoreSetting) {
|
|
throw new NoPermissionError({
|
|
message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
|
|
});
|
|
}
|
|
}
|
|
},
|
|
async query(frame) {
|
|
const stripeConnectIntegrationToken = frame.data.settings.find(setting => setting.key === 'stripe_connect_integration_token');
|
|
|
|
const settings = frame.data.settings.filter((setting) => {
|
|
// The `stripe_connect_integration_token` "setting" is only used to set the `stripe_connect_*` settings.
|
|
return ![
|
|
'stripe_connect_integration_token',
|
|
'stripe_connect_publishable_key',
|
|
'stripe_connect_secret_key',
|
|
'stripe_connect_livemode',
|
|
'stripe_connect_account_id',
|
|
'stripe_connect_display_name'
|
|
].includes(setting.key)
|
|
// Remove obfuscated settings
|
|
&& !(setting.value === settingsService.obfuscatedSetting && settingsService.isSecretSetting(setting));
|
|
});
|
|
|
|
const getSetting = setting => settingsCache.get(setting.key, {resolve: false});
|
|
|
|
const firstUnknownSetting = settings.find(setting => !getSetting(setting));
|
|
|
|
if (firstUnknownSetting) {
|
|
throw new NotFoundError({
|
|
message: i18n.t('errors.api.settings.problemFindingSetting', {
|
|
key: firstUnknownSetting.key
|
|
})
|
|
});
|
|
}
|
|
|
|
if (!(frame.options.context && frame.options.context.internal)) {
|
|
const firstCoreSetting = settings.find(setting => getSetting(setting).group === 'core');
|
|
if (firstCoreSetting) {
|
|
throw new NoPermissionError({
|
|
message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
|
|
});
|
|
}
|
|
}
|
|
|
|
if (stripeConnectIntegrationToken && stripeConnectIntegrationToken.value) {
|
|
const getSessionProp = prop => frame.original.session[prop];
|
|
try {
|
|
const data = await membersService.stripeConnect.getStripeConnectTokenData(stripeConnectIntegrationToken.value, getSessionProp);
|
|
settings.push({
|
|
key: 'stripe_connect_publishable_key',
|
|
value: data.public_key
|
|
});
|
|
settings.push({
|
|
key: 'stripe_connect_secret_key',
|
|
value: data.secret_key
|
|
});
|
|
settings.push({
|
|
key: 'stripe_connect_livemode',
|
|
value: data.livemode
|
|
});
|
|
settings.push({
|
|
key: 'stripe_connect_display_name',
|
|
value: data.display_name
|
|
});
|
|
settings.push({
|
|
key: 'stripe_connect_account_id',
|
|
value: data.account_id
|
|
});
|
|
} catch (err) {
|
|
throw new BadRequestError({
|
|
err,
|
|
message: 'The Stripe Connect token could not be parsed.'
|
|
});
|
|
}
|
|
}
|
|
|
|
return models.Settings.edit(settings, frame.options);
|
|
}
|
|
},
|
|
|
|
upload: {
|
|
headers: {
|
|
cacheInvalidate: true
|
|
},
|
|
permissions: {
|
|
method: 'edit'
|
|
},
|
|
async query(frame) {
|
|
await routeSettings.setFromFilePath(frame.file.path);
|
|
const getRoutesHash = () => frontendSettings.getCurrentHash('routes');
|
|
await settingsService.syncRoutesHash(getRoutesHash);
|
|
}
|
|
},
|
|
|
|
download: {
|
|
headers: {
|
|
disposition: {
|
|
type: 'yaml',
|
|
value: 'routes.yaml'
|
|
}
|
|
},
|
|
response: {
|
|
format: 'plain'
|
|
},
|
|
permissions: {
|
|
method: 'browse'
|
|
},
|
|
query() {
|
|
return routeSettings.get();
|
|
}
|
|
}
|
|
};
|