2019-08-09 17:11:24 +03:00
|
|
|
const Promise = require('bluebird');
|
|
|
|
const _ = require('lodash');
|
2021-06-15 21:46:27 +03:00
|
|
|
const validator = require('@tryghost/validator');
|
2019-08-09 17:11:24 +03:00
|
|
|
const models = require('../../models');
|
2020-09-09 15:28:12 +03:00
|
|
|
const frontendRouting = require('../../../frontend/services/routing');
|
|
|
|
const frontendSettings = require('../../../frontend/services/settings');
|
2021-05-03 19:29:44 +03:00
|
|
|
const i18n = require('../../../shared/i18n');
|
2020-05-28 13:40:50 +03:00
|
|
|
const {BadRequestError, NoPermissionError, NotFoundError} = require('@tryghost/errors');
|
2020-09-09 15:28:12 +03:00
|
|
|
const settingsService = require('../../services/settings');
|
2019-08-09 17:11:24 +03:00
|
|
|
const settingsCache = require('../../services/settings/cache');
|
2020-05-28 13:40:50 +03:00
|
|
|
const membersService = require('../../services/members');
|
2021-06-08 18:58:16 +03:00
|
|
|
const ghostBookshelf = require('../../models/base');
|
2019-08-09 17:11:24 +03:00
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
docName: 'settings',
|
|
|
|
|
|
|
|
browse: {
|
2021-01-26 14:15:21 +03:00
|
|
|
options: ['group'],
|
2019-08-09 17:11:24 +03:00
|
|
|
permissions: true,
|
|
|
|
query(frame) {
|
|
|
|
let settings = settingsCache.getAll();
|
|
|
|
|
|
|
|
// CASE: no context passed (functional call)
|
|
|
|
if (!frame.options.context) {
|
|
|
|
return Promise.resolve(settings.filter((setting) => {
|
2020-06-24 15:55:40 +03:00
|
|
|
return setting.group === 'site';
|
2019-08-09 17:11:24 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!frame.options.context.internal) {
|
2021-04-16 18:05:16 +03:00
|
|
|
// CASE: omit core settings unless internal request
|
2019-08-09 17:11:24 +03:00
|
|
|
settings = _.filter(settings, (setting) => {
|
2020-06-24 15:55:40 +03:00
|
|
|
const isCore = setting.group === 'core';
|
2020-06-24 16:55:50 +03:00
|
|
|
return !isCore;
|
2019-08-09 17:11:24 +03:00
|
|
|
});
|
2021-04-16 18:05:16 +03:00
|
|
|
// CASE: omit secret settings unless internal request
|
|
|
|
settings = settings.map(settingsService.hideValueIfSecret);
|
2019-08-09 17:11:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return settings;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
read: {
|
|
|
|
options: ['key'],
|
|
|
|
validation: {
|
|
|
|
options: {
|
|
|
|
key: {
|
|
|
|
required: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
permissions: {
|
|
|
|
identifier(frame) {
|
|
|
|
return frame.options.key;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
query(frame) {
|
2020-07-14 11:10:59 +03:00
|
|
|
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});
|
|
|
|
}
|
2019-08-09 17:11:24 +03:00
|
|
|
|
|
|
|
if (!setting) {
|
2020-05-22 21:22:20 +03:00
|
|
|
return Promise.reject(new NotFoundError({
|
|
|
|
message: i18n.t('errors.api.settings.problemFindingSetting', {
|
2019-08-09 17:11:24 +03:00
|
|
|
key: frame.options.key
|
|
|
|
})
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
// @TODO: handle in settings model permissible fn
|
2020-06-24 15:55:40 +03:00
|
|
|
if (setting.group === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
2020-05-22 21:22:20 +03:00
|
|
|
return Promise.reject(new NoPermissionError({
|
|
|
|
message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
|
2019-08-09 17:11:24 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2021-04-16 18:05:16 +03:00
|
|
|
setting = settingsService.hideValueIfSecret(setting);
|
|
|
|
|
2019-08-09 17:11:24 +03:00
|
|
|
return {
|
|
|
|
[frame.options.key]: setting
|
|
|
|
};
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-08-26 10:45:01 +03:00
|
|
|
validateMembersEmailUpdate: {
|
2020-06-05 19:20:04 +03:00
|
|
|
options: [
|
2020-08-26 10:45:01 +03:00
|
|
|
'token',
|
|
|
|
'action'
|
2020-06-05 19:20:04 +03:00
|
|
|
],
|
|
|
|
permissions: false,
|
|
|
|
validation: {
|
|
|
|
options: {
|
|
|
|
token: {
|
|
|
|
required: true
|
2020-08-26 10:45:01 +03:00
|
|
|
},
|
|
|
|
action: {
|
|
|
|
values: ['fromaddressupdate', 'supportaddressupdate']
|
2020-06-05 19:20:04 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
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 {
|
2020-08-26 10:45:01 +03:00
|
|
|
const {token, action} = frame.options;
|
2020-09-24 20:31:28 +03:00
|
|
|
const updatedEmailAddress = await membersService.settings.getEmailFromToken({token});
|
2020-08-26 10:45:01 +03:00
|
|
|
const actionToKeyMapping = {
|
|
|
|
fromAddressUpdate: 'members_from_address',
|
|
|
|
supportAddressUpdate: 'members_support_address'
|
|
|
|
};
|
|
|
|
if (updatedEmailAddress) {
|
2020-06-05 19:20:04 +03:00
|
|
|
return models.Settings.edit({
|
2020-08-26 10:45:01 +03:00
|
|
|
key: actionToKeyMapping[action],
|
|
|
|
value: updatedEmailAddress
|
2020-06-05 19:20:04 +03:00
|
|
|
}).then(() => {
|
|
|
|
// Redirect to Ghost-Admin settings page
|
2020-08-26 10:45:01 +03:00
|
|
|
const adminLink = membersService.settings.getAdminRedirectLink({type: action});
|
2020-06-05 19:20:04 +03:00
|
|
|
res.redirect(adminLink);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return Promise.reject(new BadRequestError({
|
|
|
|
message: 'Invalid token!'
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
return Promise.reject(new BadRequestError({
|
|
|
|
err,
|
|
|
|
message: 'Invalid token!'
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-08-26 10:45:01 +03:00
|
|
|
updateMembersEmail: {
|
2020-06-05 19:20:04 +03:00
|
|
|
permissions: {
|
|
|
|
method: 'edit'
|
|
|
|
},
|
2020-08-26 10:45:01 +03:00
|
|
|
data: [
|
|
|
|
'email',
|
|
|
|
'type'
|
|
|
|
],
|
2020-06-05 19:20:04 +03:00
|
|
|
async query(frame) {
|
2020-08-26 10:45:01 +03:00
|
|
|
const {email, type} = frame.data;
|
2020-06-05 19:20:04 +03:00
|
|
|
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
|
|
|
throw new BadRequestError({
|
|
|
|
message: i18n.t('errors.api.settings.invalidEmailReceived')
|
|
|
|
});
|
|
|
|
}
|
2020-08-26 10:45:01 +03:00
|
|
|
|
|
|
|
if (!type || !['fromAddressUpdate', 'supportAddressUpdate'].includes(type)) {
|
|
|
|
throw new BadRequestError({
|
|
|
|
message: 'Invalid email type recieved'
|
|
|
|
});
|
|
|
|
}
|
2020-06-05 19:20:04 +03:00
|
|
|
try {
|
|
|
|
// Send magic link to update fromAddress
|
2020-08-26 10:45:01 +03:00
|
|
|
await membersService.settings.sendEmailAddressUpdateMagicLink({
|
|
|
|
email,
|
|
|
|
type
|
2020-06-05 19:20:04 +03:00
|
|
|
});
|
|
|
|
} catch (err) {
|
|
|
|
throw new BadRequestError({
|
|
|
|
err,
|
|
|
|
message: i18n.t('errors.mail.failedSendingEmail.error')
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-06-18 19:07:02 +03:00
|
|
|
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.'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-06-08 18:58:16 +03:00
|
|
|
/** Delete all Stripe data from DB */
|
2021-06-08 19:42:17 +03:00
|
|
|
await ghostBookshelf.knex.raw(`
|
|
|
|
UPDATE products SET monthly_price_id = null, yearly_price_id = null
|
|
|
|
`);
|
2021-06-08 18:58:16 +03:00
|
|
|
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
|
|
|
|
`);
|
|
|
|
|
2020-06-29 17:22:42 +03:00
|
|
|
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);
|
2020-06-18 19:07:02 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-09 17:11:24 +03:00
|
|
|
edit: {
|
|
|
|
headers: {
|
|
|
|
cacheInvalidate: true
|
|
|
|
},
|
|
|
|
permissions: {
|
2019-10-09 11:26:54 +03:00
|
|
|
unsafeAttrsObject(frame) {
|
|
|
|
return _.find(frame.data.settings, {key: 'labs'});
|
|
|
|
},
|
2020-05-28 13:40:47 +03:00
|
|
|
async before(frame) {
|
|
|
|
if (frame.options.context && frame.options.context.internal) {
|
|
|
|
return;
|
|
|
|
}
|
2019-08-09 17:11:24 +03:00
|
|
|
|
2020-06-24 15:55:40 +03:00
|
|
|
const firstCoreSetting = frame.data.settings.find(setting => setting.group === 'core');
|
2020-05-28 13:40:47 +03:00
|
|
|
if (firstCoreSetting) {
|
|
|
|
throw new NoPermissionError({
|
|
|
|
message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
|
|
|
|
});
|
2019-08-09 17:11:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2020-05-28 13:40:47 +03:00
|
|
|
async query(frame) {
|
2020-05-28 13:40:50 +03:00
|
|
|
const stripeConnectIntegrationToken = frame.data.settings.find(setting => setting.key === 'stripe_connect_integration_token');
|
|
|
|
|
|
|
|
const settings = frame.data.settings.filter((setting) => {
|
2021-04-16 18:05:16 +03:00
|
|
|
// The `stripe_connect_integration_token` "setting" is only used to set the `stripe_connect_*` settings.
|
2020-06-30 12:27:43 +03:00
|
|
|
return ![
|
|
|
|
'stripe_connect_integration_token',
|
|
|
|
'stripe_connect_publishable_key',
|
|
|
|
'stripe_connect_secret_key',
|
|
|
|
'stripe_connect_livemode',
|
|
|
|
'stripe_connect_account_id',
|
|
|
|
'stripe_connect_display_name'
|
2021-04-16 18:05:16 +03:00
|
|
|
].includes(setting.key)
|
|
|
|
// Remove obfuscated settings
|
|
|
|
&& !(setting.value === settingsService.obfuscatedSetting && settingsService.isSecretSetting(setting));
|
2020-05-28 13:40:50 +03:00
|
|
|
});
|
2019-08-09 17:11:24 +03:00
|
|
|
|
2020-05-28 13:40:47 +03:00
|
|
|
const getSetting = setting => settingsCache.get(setting.key, {resolve: false});
|
2019-08-09 17:11:24 +03:00
|
|
|
|
2020-05-28 13:40:47 +03:00
|
|
|
const firstUnknownSetting = settings.find(setting => !getSetting(setting));
|
2019-08-09 17:11:24 +03:00
|
|
|
|
2020-05-28 13:40:47 +03:00
|
|
|
if (firstUnknownSetting) {
|
|
|
|
throw new NotFoundError({
|
|
|
|
message: i18n.t('errors.api.settings.problemFindingSetting', {
|
|
|
|
key: firstUnknownSetting.key
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
2019-08-09 17:11:24 +03:00
|
|
|
|
2020-05-28 13:40:47 +03:00
|
|
|
if (!(frame.options.context && frame.options.context.internal)) {
|
2020-06-24 15:55:40 +03:00
|
|
|
const firstCoreSetting = settings.find(setting => getSetting(setting).group === 'core');
|
2020-05-28 13:40:47 +03:00
|
|
|
if (firstCoreSetting) {
|
|
|
|
throw new NoPermissionError({
|
2020-05-22 21:22:20 +03:00
|
|
|
message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
|
2020-05-28 13:40:47 +03:00
|
|
|
});
|
2019-08-09 17:11:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-02 19:58:30 +03:00
|
|
|
if (stripeConnectIntegrationToken && stripeConnectIntegrationToken.value) {
|
2020-05-28 13:40:50 +03:00
|
|
|
const getSessionProp = prop => frame.original.session[prop];
|
|
|
|
try {
|
|
|
|
const data = await membersService.stripeConnect.getStripeConnectTokenData(stripeConnectIntegrationToken.value, getSessionProp);
|
|
|
|
settings.push({
|
2020-06-29 17:22:42 +03:00
|
|
|
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
|
2020-05-28 13:40:50 +03:00
|
|
|
});
|
|
|
|
} catch (err) {
|
|
|
|
throw new BadRequestError({
|
2020-06-02 19:58:30 +03:00
|
|
|
err,
|
2020-05-28 13:40:50 +03:00
|
|
|
message: 'The Stripe Connect token could not be parsed.'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:40:47 +03:00
|
|
|
return models.Settings.edit(settings, frame.options);
|
2019-08-09 17:11:24 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
upload: {
|
|
|
|
headers: {
|
|
|
|
cacheInvalidate: true
|
|
|
|
},
|
|
|
|
permissions: {
|
|
|
|
method: 'edit'
|
|
|
|
},
|
2020-09-09 15:28:12 +03:00
|
|
|
async query(frame) {
|
|
|
|
await frontendRouting.settings.setFromFilePath(frame.file.path);
|
|
|
|
const getRoutesHash = () => frontendSettings.getCurrentHash('routes');
|
|
|
|
await settingsService.syncRoutesHash(getRoutesHash);
|
2019-08-09 17:11:24 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
download: {
|
|
|
|
headers: {
|
|
|
|
disposition: {
|
|
|
|
type: 'yaml',
|
|
|
|
value: 'routes.yaml'
|
|
|
|
}
|
|
|
|
},
|
|
|
|
response: {
|
|
|
|
format: 'plain'
|
|
|
|
},
|
|
|
|
permissions: {
|
|
|
|
method: 'browse'
|
|
|
|
},
|
|
|
|
query() {
|
2020-09-09 15:28:12 +03:00
|
|
|
return frontendRouting.settings.get();
|
2019-08-09 17:11:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|