mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
closes https://github.com/TryGhost/Team/issues/724 closes https://github.com/TryGhost/Team/issues/739 Currently, site owners are allowed to disconnect Stripe if they don't have any active subscriptions for a member. On disconnect, all stripe related data for the old account in DB should be cleared as using Stripe id for old account can cause weird failures due to incorrect Stripe key being used. This was also causing site owners to not be able to create new prices after connecting to new account as it ended up using old stripe product id which failed on Stripe request.
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
const Promise = require('bluebird');
|
|
const _ = require('lodash');
|
|
const validator = require('validator');
|
|
const models = require('../../models');
|
|
const frontendRouting = require('../../../frontend/services/routing');
|
|
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('../../services/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(`
|
|
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 frontendRouting.settings.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 frontendRouting.settings.get();
|
|
}
|
|
}
|
|
};
|