ghost 4.15.0 → 4.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +7 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -1
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/package.json +1 -1
- package/content/themes/casper/page.hbs +28 -26
- package/content/themes/casper/partials/post-card.hbs +2 -2
- package/content/themes/casper/post.hbs +67 -65
- package/content/themes/casper/tag.hbs +2 -2
- package/core/boot.js +7 -7
- package/core/bridge.js +4 -3
- package/core/built/assets/{chunk.3.4b1d9e20e57164ac9c29.js → chunk.3.b80d3e1e6b8556aaff3c.js} +72 -71
- package/core/built/assets/ghost-dark-f7bf2dd8d8c702716f75bfa4ccd92df2.css +1 -0
- package/core/built/assets/{ghost.min-e35cfee26d942c364166f57f3dcc9e75.js → ghost.min-52a5420ffcea6bf17761b5c59cf020e2.js} +979 -908
- package/core/built/assets/ghost.min-741246f42f000c073999a5363434ea2c.css +1 -0
- package/core/built/assets/icons/discount-bubble.svg +1 -0
- package/core/built/assets/{vendor.min-ca33abc718f21a51327841d58f8875d0.js → vendor.min-1bfc9d56d27508db88ef417deb55f16f.js} +454 -434
- package/core/frontend/apps/amp/lib/helpers/amp_analytics.js +2 -2
- package/core/frontend/apps/amp/lib/helpers/amp_components.js +2 -1
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +5 -1
- package/core/frontend/apps/amp/lib/helpers/amp_style.js +1 -1
- package/core/frontend/apps/amp/lib/router.js +8 -4
- package/core/frontend/apps/private-blogging/index.js +13 -5
- package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +1 -1
- package/core/frontend/apps/private-blogging/lib/middleware.js +8 -3
- package/core/frontend/helpers/asset.js +10 -2
- package/core/frontend/helpers/author.js +5 -3
- package/core/frontend/helpers/authors.js +4 -3
- package/core/frontend/helpers/body_class.js +1 -1
- package/core/frontend/helpers/cancel_link.js +9 -2
- package/core/frontend/helpers/concat.js +1 -1
- package/core/frontend/helpers/content.js +1 -1
- package/core/frontend/helpers/date.js +1 -1
- package/core/frontend/helpers/encode.js +1 -1
- package/core/frontend/helpers/excerpt.js +2 -1
- package/core/frontend/helpers/facebook_url.js +2 -1
- package/core/frontend/helpers/foreach.js +11 -2
- package/core/frontend/helpers/get.js +14 -3
- package/core/frontend/helpers/ghost_foot.js +2 -1
- package/core/frontend/helpers/ghost_head.js +10 -1
- package/core/frontend/helpers/has.js +8 -3
- package/core/frontend/helpers/img_url.js +9 -3
- package/core/frontend/helpers/is.js +7 -2
- package/core/frontend/helpers/lang.js +1 -1
- package/core/frontend/helpers/link.js +11 -2
- package/core/frontend/helpers/link_class.js +11 -2
- package/core/frontend/helpers/match.js +12 -3
- package/core/frontend/helpers/navigation.js +13 -4
- package/core/frontend/helpers/pagination.js +15 -5
- package/core/frontend/helpers/plural.js +8 -2
- package/core/frontend/helpers/post_class.js +1 -1
- package/core/frontend/helpers/prev_post.js +9 -2
- package/core/frontend/helpers/price.js +11 -6
- package/core/frontend/helpers/products.js +2 -1
- package/core/frontend/helpers/reading_time.js +4 -2
- package/core/frontend/helpers/t.js +1 -1
- package/core/frontend/helpers/tags.js +3 -1
- package/core/frontend/helpers/title.js +1 -1
- package/core/frontend/helpers/twitter_url.js +2 -1
- package/core/frontend/helpers/url.js +3 -1
- package/core/frontend/services/proxy.js +34 -57
- package/core/frontend/services/rendering.js +24 -0
- package/core/frontend/services/routing/controllers/channel.js +6 -2
- package/core/frontend/services/routing/controllers/collection.js +6 -2
- package/core/frontend/services/routing/middlewares/page-param.js +6 -2
- package/core/frontend/services/theme-engine/middleware.js +23 -6
- package/core/frontend/services/theme-engine/preview.js +31 -8
- package/core/server/adapters/scheduling/post-scheduling/scheduler-intergation.js +6 -4
- package/core/server/adapters/storage/LocalFileStorage.js +10 -4
- package/core/server/api/canary/custom-theme-settings.js +22 -0
- package/core/server/api/canary/index.js +4 -0
- package/core/server/api/canary/members.js +1 -1
- package/core/server/api/canary/redirects.js +5 -5
- package/core/server/api/canary/settings.js +16 -148
- package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +13 -0
- package/core/server/api/canary/utils/serializers/output/index.js +4 -0
- package/core/server/api/canary/utils/validators/input/settings.js +23 -1
- package/core/server/api/v2/redirects.js +3 -3
- package/core/server/api/v2/settings.js +3 -4
- package/core/server/api/v3/redirects.js +5 -5
- package/core/server/api/v3/settings.js +16 -136
- package/core/server/api/v3/utils/validators/input/settings.js +23 -1
- package/core/server/data/db/state-manager.js +1 -1
- package/core/server/data/exporter/table-lists.js +3 -1
- package/core/server/data/importer/import-manager.js +398 -0
- package/core/server/data/importer/importers/data/data-importer.js +162 -0
- package/core/server/data/importer/importers/data/index.js +1 -162
- package/core/server/data/importer/index.js +1 -379
- package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
- package/core/server/data/migrations/versions/4.17/01-add-custom-theme-settings-permissions.js +21 -0
- package/core/server/data/migrations/versions/4.17/02-add-offers-table.js +19 -0
- package/core/server/data/migrations/versions/4.17/03-add-offers-permissions.js +35 -0
- package/core/server/data/schema/fixtures/fixtures.json +32 -0
- package/core/server/data/schema/schema.js +33 -0
- package/core/server/models/custom-theme-setting.js +9 -0
- package/core/server/models/index.js +2 -0
- package/core/server/services/custom-theme-settings.js +8 -0
- package/core/server/services/members/api.js +4 -1
- package/core/server/services/redirects/index.js +15 -0
- package/core/{frontend → server}/services/redirects/settings.js +13 -6
- package/core/server/services/redirects/validation.js +44 -0
- package/core/{frontend/services/settings → server/services/route-settings}/default-routes.yaml +0 -0
- package/core/server/services/route-settings/default-settings-manager.js +62 -0
- package/core/server/services/route-settings/index.js +32 -1
- package/core/server/services/route-settings/route-settings.js +38 -12
- package/core/server/services/route-settings/settings-loader.js +102 -0
- package/core/{frontend/services/settings → server/services/route-settings}/validate.js +38 -28
- package/core/server/services/route-settings/yaml-parser.js +53 -0
- package/core/server/services/settings/index.js +13 -16
- package/core/server/services/settings/settings-bread-service.js +188 -0
- package/core/server/services/settings/settings-utils.js +32 -0
- package/core/server/services/themes/ThemeStorage.js +5 -4
- package/core/server/services/themes/activation-bridge.js +14 -0
- package/core/server/services/themes/validate.js +5 -2
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/canary/admin/routes.js +5 -1
- package/core/server/web/members/app.js +3 -0
- package/core/server/web/oauth/app.js +7 -8
- package/core/server/web/shared/middlewares/custom-redirects.js +82 -59
- package/core/server/web/site/routes.js +2 -2
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/overrides.json +1 -1
- package/core/shared/custom-theme-settings-cache.js +3 -0
- package/core/shared/i18n/translations/en.json +2 -13
- package/core/shared/labs.js +2 -2
- package/package.json +42 -41
- package/yarn.lock +916 -901
- package/core/built/assets/ghost-dark-faf931d90e92535e6c03ca16793cbe7b.css +0 -1
- package/core/built/assets/ghost.min-7aa074ad556a8455155ac88ceaca03ab.css +0 -1
- package/core/frontend/services/redirects/index.js +0 -9
- package/core/frontend/services/redirects/validation.js +0 -28
- package/core/frontend/services/settings/ensure-settings.js +0 -47
- package/core/frontend/services/settings/index.js +0 -104
- package/core/frontend/services/settings/loader.js +0 -89
- package/core/frontend/services/settings/yaml-parser.js +0 -31
|
@@ -5,22 +5,18 @@
|
|
|
5
5
|
const events = require('../../lib/common/events');
|
|
6
6
|
const models = require('../../models');
|
|
7
7
|
const SettingsCache = require('../../../shared/settings-cache');
|
|
8
|
+
const SettingsBREADService = require('./settings-bread-service');
|
|
9
|
+
const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function hideValueIfSecret(setting) {
|
|
19
|
-
if (setting.value && isSecretSetting(setting)) {
|
|
20
|
-
return {...setting, value: obfuscatedSetting};
|
|
21
|
-
}
|
|
22
|
-
return setting;
|
|
23
|
-
}
|
|
11
|
+
/**
|
|
12
|
+
* @returns {SettingsBREADService} instance of the PostsService
|
|
13
|
+
*/
|
|
14
|
+
const getSettingsBREADServiceInstance = () => {
|
|
15
|
+
return new SettingsBREADService({
|
|
16
|
+
SettingsModel: models.Settings,
|
|
17
|
+
settingsCache: SettingsCache
|
|
18
|
+
});
|
|
19
|
+
};
|
|
24
20
|
|
|
25
21
|
module.exports = {
|
|
26
22
|
/**
|
|
@@ -74,5 +70,6 @@ module.exports = {
|
|
|
74
70
|
|
|
75
71
|
obfuscatedSetting,
|
|
76
72
|
isSecretSetting,
|
|
77
|
-
hideValueIfSecret
|
|
73
|
+
hideValueIfSecret,
|
|
74
|
+
getSettingsBREADServiceInstance
|
|
78
75
|
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const tpl = require('@tryghost/tpl');
|
|
3
|
+
const {NotFoundError, NoPermissionError, BadRequestError} = require('@tryghost/errors');
|
|
4
|
+
const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
problemFindingSetting: 'Problem finding setting: {key}',
|
|
8
|
+
accessCoreSettingFromExtReq: 'Attempted to access core setting from external request'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class SettingsBREADService {
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {Object} options.SettingsModel
|
|
16
|
+
* @param {Object} options.settingsCache - SettingsCache instance
|
|
17
|
+
*/
|
|
18
|
+
constructor({SettingsModel, settingsCache}) {
|
|
19
|
+
this.SettingsModel = SettingsModel;
|
|
20
|
+
this.settingsCache = settingsCache;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} context ghost API context instance
|
|
26
|
+
* @returns
|
|
27
|
+
*/
|
|
28
|
+
browse(context) {
|
|
29
|
+
let settings = this.settingsCache.getAll();
|
|
30
|
+
|
|
31
|
+
// CASE: no context passed (functional call)
|
|
32
|
+
if (!context) {
|
|
33
|
+
return Promise.resolve(settings.filter((setting) => {
|
|
34
|
+
return setting.group === 'site';
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!context.internal) {
|
|
39
|
+
// CASE: omit core settings unless internal request
|
|
40
|
+
settings = _.filter(settings, (setting) => {
|
|
41
|
+
const isCore = setting.group === 'core';
|
|
42
|
+
return !isCore;
|
|
43
|
+
});
|
|
44
|
+
// CASE: omit secret settings unless internal request
|
|
45
|
+
settings = settings.map(hideValueIfSecret);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return settings;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
* @param {String} key setting key
|
|
54
|
+
* @param {Object} [context] API context instance
|
|
55
|
+
* @returns {Object} an object with a filled out key that comes in a parameter
|
|
56
|
+
*/
|
|
57
|
+
read(key, context) {
|
|
58
|
+
let setting;
|
|
59
|
+
|
|
60
|
+
if (key === 'slack') {
|
|
61
|
+
const slackURL = this.settingsCache.get('slack_url', {resolve: false});
|
|
62
|
+
const slackUsername = this.settingsCache.get('slack_username', {resolve: false});
|
|
63
|
+
|
|
64
|
+
setting = slackURL || slackUsername;
|
|
65
|
+
setting.key = 'slack';
|
|
66
|
+
setting.value = [{
|
|
67
|
+
url: slackURL && slackURL.value,
|
|
68
|
+
username: slackUsername && slackUsername.value
|
|
69
|
+
}];
|
|
70
|
+
} else {
|
|
71
|
+
setting = this.settingsCache.get(key, {resolve: false});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!setting) {
|
|
75
|
+
return Promise.reject(new NotFoundError({
|
|
76
|
+
message: tpl(messages.problemFindingSetting, {
|
|
77
|
+
key: key
|
|
78
|
+
})
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// @TODO: handle in settings model permissible fn
|
|
83
|
+
if (setting.group === 'core' && !(context && context.internal)) {
|
|
84
|
+
return Promise.reject(new NoPermissionError({
|
|
85
|
+
message: tpl(messages.accessCoreSettingFromExtReq)
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setting = hideValueIfSecret(setting);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
[key]: setting
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
*
|
|
98
|
+
* @param {Object[]} settings
|
|
99
|
+
* @param {Object} options
|
|
100
|
+
* @param {Object} [options.context]
|
|
101
|
+
* @param {Object} [stripeConnectData]
|
|
102
|
+
* @returns
|
|
103
|
+
*/
|
|
104
|
+
async edit(settings, options, stripeConnectData) {
|
|
105
|
+
const filteredSettings = settings.filter((setting) => {
|
|
106
|
+
// The `stripe_connect_integration_token` "setting" is only used to set the `stripe_connect_*` settings.
|
|
107
|
+
return ![
|
|
108
|
+
'stripe_connect_integration_token',
|
|
109
|
+
'stripe_connect_publishable_key',
|
|
110
|
+
'stripe_connect_secret_key',
|
|
111
|
+
'stripe_connect_livemode',
|
|
112
|
+
'stripe_connect_account_id',
|
|
113
|
+
'stripe_connect_display_name'
|
|
114
|
+
].includes(setting.key)
|
|
115
|
+
// Remove obfuscated settings
|
|
116
|
+
&& !(setting.value === obfuscatedSetting && isSecretSetting(setting));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const getSetting = setting => this.settingsCache.get(setting.key, {resolve: false});
|
|
120
|
+
|
|
121
|
+
const firstUnknownSetting = filteredSettings.find(setting => !getSetting(setting));
|
|
122
|
+
|
|
123
|
+
if (firstUnknownSetting) {
|
|
124
|
+
throw new NotFoundError({
|
|
125
|
+
message: tpl(messages.problemFindingSetting, {
|
|
126
|
+
key: firstUnknownSetting.key
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!(options.context && options.context.internal)) {
|
|
132
|
+
const firstCoreSetting = filteredSettings.find(setting => getSetting(setting).group === 'core');
|
|
133
|
+
|
|
134
|
+
if (firstCoreSetting) {
|
|
135
|
+
throw new NoPermissionError({
|
|
136
|
+
message: tpl(messages.accessCoreSettingFromExtReq)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (stripeConnectData) {
|
|
142
|
+
filteredSettings.push({
|
|
143
|
+
key: 'stripe_connect_publishable_key',
|
|
144
|
+
value: stripeConnectData.public_key
|
|
145
|
+
});
|
|
146
|
+
filteredSettings.push({
|
|
147
|
+
key: 'stripe_connect_secret_key',
|
|
148
|
+
value: stripeConnectData.secret_key
|
|
149
|
+
});
|
|
150
|
+
filteredSettings.push({
|
|
151
|
+
key: 'stripe_connect_livemode',
|
|
152
|
+
value: stripeConnectData.livemode
|
|
153
|
+
});
|
|
154
|
+
filteredSettings.push({
|
|
155
|
+
key: 'stripe_connect_display_name',
|
|
156
|
+
value: stripeConnectData.display_name
|
|
157
|
+
});
|
|
158
|
+
filteredSettings.push({
|
|
159
|
+
key: 'stripe_connect_account_id',
|
|
160
|
+
value: stripeConnectData.account_id
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return this.SettingsModel.edit(filteredSettings, options);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
*
|
|
169
|
+
* @param {Object} stripeConnectIntegrationToken
|
|
170
|
+
* @param {Function} getSessionProp sync function fetching property from session store
|
|
171
|
+
* @param {Function} getStripeConnectTokenData async function retreiving Stripe Connect data for settings
|
|
172
|
+
* @returns {Promise<Object>} resolves with an object with following keys: public_key, secret_key, livemode, display_name, account_id
|
|
173
|
+
*/
|
|
174
|
+
async getStripeConnectData(stripeConnectIntegrationToken, getSessionProp, getStripeConnectTokenData) {
|
|
175
|
+
if (stripeConnectIntegrationToken && stripeConnectIntegrationToken.value) {
|
|
176
|
+
try {
|
|
177
|
+
return await getStripeConnectTokenData(stripeConnectIntegrationToken.value, getSessionProp);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw new BadRequestError({
|
|
180
|
+
err,
|
|
181
|
+
message: 'The Stripe Connect token could not be parsed.'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = SettingsBREADService;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The string returned when a setting is set as write-only
|
|
2
|
+
const obfuscatedSetting = '••••••••';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @description // The function used to decide whether a setting is write-only
|
|
6
|
+
* @param {Object} setting setting record
|
|
7
|
+
* @param {String} setting.key
|
|
8
|
+
* @returns {Boolean}
|
|
9
|
+
*/
|
|
10
|
+
function isSecretSetting(setting) {
|
|
11
|
+
return /secret/.test(setting.key);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @description The function that obfuscates a write-only setting
|
|
16
|
+
* @param {Object} setting setting record
|
|
17
|
+
* @param {String} setting.value
|
|
18
|
+
* @param {String} setting.key
|
|
19
|
+
* @returns {Object} settings record with obfuscated value if it's a secret
|
|
20
|
+
*/
|
|
21
|
+
function hideValueIfSecret(setting) {
|
|
22
|
+
if (setting.value && isSecretSetting(setting)) {
|
|
23
|
+
return {...setting, value: obfuscatedSetting};
|
|
24
|
+
}
|
|
25
|
+
return setting;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
obfuscatedSetting,
|
|
30
|
+
isSecretSetting,
|
|
31
|
+
hideValueIfSecret
|
|
32
|
+
};
|
|
@@ -60,8 +60,8 @@ class ThemeStorage extends LocalFileStorage {
|
|
|
60
60
|
/**
|
|
61
61
|
* Rename a file / folder
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
* @param String
|
|
63
|
+
* @param {String} srcName
|
|
64
|
+
* @param {String} destName
|
|
65
65
|
*/
|
|
66
66
|
rename(srcName, destName) {
|
|
67
67
|
let src = path.join(this.getTargetDir(), srcName);
|
|
@@ -71,9 +71,10 @@ class ThemeStorage extends LocalFileStorage {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
74
|
+
* Remove a file / folder
|
|
75
75
|
*
|
|
76
|
-
* @param String
|
|
76
|
+
* @param {String} fileName
|
|
77
|
+
* @returns {Promise<void>}
|
|
77
78
|
*/
|
|
78
79
|
delete(fileName) {
|
|
79
80
|
return fs.remove(path.join(this.getTargetDir(), fileName));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('themes');
|
|
2
2
|
const bridge = require('../../../bridge');
|
|
3
|
+
const labs = require('../../../shared/labs');
|
|
4
|
+
const customThemeSettings = require('../custom-theme-settings');
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* These helper methods mean that the bridge is only required in one place
|
|
@@ -8,14 +10,26 @@ const bridge = require('../../../bridge');
|
|
|
8
10
|
module.exports = {
|
|
9
11
|
activateFromBoot: (themeName, theme, checkedTheme) => {
|
|
10
12
|
debug('Activating theme (method A on boot)', themeName);
|
|
13
|
+
// TODO: probably a better place for this to happen - after successful activation / when reloading site?
|
|
14
|
+
if (labs.isSet('customThemeSettings')) {
|
|
15
|
+
customThemeSettings.activateTheme(checkedTheme);
|
|
16
|
+
}
|
|
11
17
|
bridge.activateTheme(theme, checkedTheme);
|
|
12
18
|
},
|
|
13
19
|
activateFromAPI: (themeName, theme, checkedTheme) => {
|
|
14
20
|
debug('Activating theme (method B on API "activate")', themeName);
|
|
21
|
+
// TODO: probably a better place for this to happen - after successful activation / when reloading site?
|
|
22
|
+
if (labs.isSet('customThemeSettings')) {
|
|
23
|
+
customThemeSettings.activateTheme(checkedTheme);
|
|
24
|
+
}
|
|
15
25
|
bridge.activateTheme(theme, checkedTheme);
|
|
16
26
|
},
|
|
17
27
|
activateFromAPIOverride: (themeName, theme, checkedTheme) => {
|
|
18
28
|
debug('Activating theme (method C on API "override")', themeName);
|
|
29
|
+
// TODO: probably a better place for this to happen - after successful activation / when reloading site?
|
|
30
|
+
if (labs.isSet('customThemeSettings')) {
|
|
31
|
+
customThemeSettings.activateTheme(checkedTheme);
|
|
32
|
+
}
|
|
19
33
|
bridge.activateTheme(theme, checkedTheme);
|
|
20
34
|
}
|
|
21
35
|
};
|
|
@@ -2,6 +2,7 @@ const debug = require('@tryghost/debug')('themes');
|
|
|
2
2
|
const _ = require('lodash');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const config = require('../../../shared/config');
|
|
5
|
+
const labs = require('../../../shared/labs');
|
|
5
6
|
const tpl = require('@tryghost/tpl');
|
|
6
7
|
const errors = require('@tryghost/errors');
|
|
7
8
|
|
|
@@ -27,12 +28,14 @@ const check = async function check(theme, isZip) {
|
|
|
27
28
|
debug('zip mode');
|
|
28
29
|
checkedTheme = await gscan.checkZip(theme, {
|
|
29
30
|
keepExtractedDir: true,
|
|
30
|
-
checkVersion: 'canary'
|
|
31
|
+
checkVersion: 'canary',
|
|
32
|
+
labs: labs.getAll()
|
|
31
33
|
});
|
|
32
34
|
} else {
|
|
33
35
|
debug('non-zip mode');
|
|
34
36
|
checkedTheme = await gscan.check(theme.path, {
|
|
35
|
-
checkVersion: 'canary'
|
|
37
|
+
checkVersion: 'canary',
|
|
38
|
+
labs: labs.getAll()
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Ghost Admin</title>
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.
|
|
11
|
+
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.17%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
<link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
|
|
44
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
44
|
+
<link rel="stylesheet" href="assets/ghost.min-741246f42f000c073999a5363434ea2c.css" title="light">
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
<script src="assets/vendor.min-
|
|
63
|
-
<script src="assets/ghost.min-
|
|
62
|
+
<script src="assets/vendor.min-1bfc9d56d27508db88ef417deb55f16f.js"></script>
|
|
63
|
+
<script src="assets/ghost.min-52a5420ffcea6bf17761b5c59cf020e2.js"></script>
|
|
64
64
|
|
|
65
65
|
</body>
|
|
66
66
|
</html>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Ghost Admin</title>
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.
|
|
11
|
+
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.17%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
<link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
|
|
44
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
44
|
+
<link rel="stylesheet" href="assets/ghost.min-741246f42f000c073999a5363434ea2c.css" title="light">
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
<script src="assets/vendor.min-
|
|
63
|
-
<script src="assets/ghost.min-
|
|
62
|
+
<script src="assets/vendor.min-1bfc9d56d27508db88ef417deb55f16f.js"></script>
|
|
63
|
+
<script src="assets/ghost.min-52a5420ffcea6bf17761b5c59cf020e2.js"></script>
|
|
64
64
|
|
|
65
65
|
</body>
|
|
66
66
|
</html>
|
|
@@ -98,7 +98,7 @@ module.exports = function apiRoutes() {
|
|
|
98
98
|
router.get('/members', mw.authAdminApi, http(api.members.browse));
|
|
99
99
|
router.post('/members', mw.authAdminApi, http(api.members.add));
|
|
100
100
|
router.del('/members', mw.authAdminApi, http(api.members.bulkDestroy));
|
|
101
|
-
router.put('/members/bulk',
|
|
101
|
+
router.put('/members/bulk', mw.authAdminApi, http(api.members.bulkEdit));
|
|
102
102
|
|
|
103
103
|
router.get('/members/stats/count', mw.authAdminApi, http(api.members.memberStats));
|
|
104
104
|
router.get('/members/stats/mrr', mw.authAdminApi, http(api.members.mrrStats));
|
|
@@ -273,5 +273,9 @@ module.exports = function apiRoutes() {
|
|
|
273
273
|
router.put('/snippets/:id', mw.authAdminApi, http(api.snippets.edit));
|
|
274
274
|
router.del('/snippets/:id', mw.authAdminApi, http(api.snippets.destroy));
|
|
275
275
|
|
|
276
|
+
// ## Custom theme settings
|
|
277
|
+
router.get('/custom_theme_settings', mw.authAdminApi, labs.enabledMiddleware('customThemeSettings'), http(api.customThemeSettings.browse));
|
|
278
|
+
router.put('/custom_theme_settings', mw.authAdminApi, labs.enabledMiddleware('customThemeSettings'), http(api.customThemeSettings.edit));
|
|
279
|
+
|
|
276
280
|
return router;
|
|
277
281
|
};
|
|
@@ -7,6 +7,7 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
7
7
|
const membersService = require('../../services/members');
|
|
8
8
|
const middleware = membersService.middleware;
|
|
9
9
|
const shared = require('../shared');
|
|
10
|
+
const labs = require('../../../shared/labs');
|
|
10
11
|
|
|
11
12
|
module.exports = function setupMembersApp() {
|
|
12
13
|
debug('Members App setup start');
|
|
@@ -34,6 +35,7 @@ module.exports = function setupMembersApp() {
|
|
|
34
35
|
// We don't want to add global bodyParser middleware as that interfers with stripe webhook requests on - `/webhooks`.
|
|
35
36
|
membersApp.get('/api/member', middleware.getMemberData);
|
|
36
37
|
membersApp.put('/api/member', bodyParser.json({limit: '1mb'}), middleware.updateMemberData);
|
|
38
|
+
membersApp.post('/api/member/email', bodyParser.json({limit: '1mb'}), (req, res) => membersService.api.middleware.updateEmailAddress(req, res));
|
|
37
39
|
membersApp.get('/api/session', middleware.getIdentityToken);
|
|
38
40
|
membersApp.delete('/api/session', middleware.deleteSession);
|
|
39
41
|
membersApp.get('/api/site', middleware.getMemberSiteData);
|
|
@@ -43,6 +45,7 @@ module.exports = function setupMembersApp() {
|
|
|
43
45
|
membersApp.post('/api/create-stripe-checkout-session', (req, res, next) => membersService.api.middleware.createCheckoutSession(req, res, next));
|
|
44
46
|
membersApp.post('/api/create-stripe-update-session', (req, res, next) => membersService.api.middleware.createCheckoutSetupSession(req, res, next));
|
|
45
47
|
membersApp.put('/api/subscriptions/:id', (req, res, next) => membersService.api.middleware.updateSubscription(req, res, next));
|
|
48
|
+
membersApp.post('/api/events', labs.enabledMiddleware('membersActivity'), middleware.loadMemberSession, (req, res, next) => membersService.api.middleware.createEvents(req, res, next));
|
|
46
49
|
|
|
47
50
|
// API error handling
|
|
48
51
|
membersApp.use('/api', shared.middlewares.errorHandler.resourceNotFound);
|
|
@@ -36,11 +36,10 @@ module.exports = function setupOAuthApp() {
|
|
|
36
36
|
*/
|
|
37
37
|
function googleOAuthMiddleware(clientId, secret) {
|
|
38
38
|
return (req, res, next) => {
|
|
39
|
-
|
|
39
|
+
const adminURL = urlUtils.urlFor('admin', true);
|
|
40
40
|
|
|
41
41
|
//Create the callback url to be sent to Google
|
|
42
|
-
const callbackUrl = new URL(
|
|
43
|
-
callbackUrl.pathname = '/ghost/oauth/google/callback';
|
|
42
|
+
const callbackUrl = new URL('oauth/google/callback', adminURL);
|
|
44
43
|
|
|
45
44
|
passport.authenticate(new GoogleStrategy({
|
|
46
45
|
clientID: clientId,
|
|
@@ -55,7 +54,7 @@ module.exports = function setupOAuthApp() {
|
|
|
55
54
|
const emails = profile.emails.filter(email => email.verified === true).map(email => email.value);
|
|
56
55
|
|
|
57
56
|
if (!emails.includes(req.user.get('email'))) {
|
|
58
|
-
return res.redirect('
|
|
57
|
+
return res.redirect(new URL('#/staff?message=oauth-linking-failed', adminURL));
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
// TODO: configure the oauth data for this user (row in the oauth table)
|
|
@@ -70,7 +69,7 @@ module.exports = function setupOAuthApp() {
|
|
|
70
69
|
//TODO: instead find the oauth row with the email use the provider id
|
|
71
70
|
const emails = profile.emails.filter(email => email.verified === true);
|
|
72
71
|
if (emails.length < 1) {
|
|
73
|
-
return res.redirect('
|
|
72
|
+
return res.redirect(new URL('#/signin?message=login-failed', adminURL));
|
|
74
73
|
}
|
|
75
74
|
const email = emails[0].value;
|
|
76
75
|
|
|
@@ -85,7 +84,7 @@ module.exports = function setupOAuthApp() {
|
|
|
85
84
|
let invite = await models.Invite.findOne({email, status: 'sent'}, options);
|
|
86
85
|
|
|
87
86
|
if (!invite || invite.get('expires') < Date.now()) {
|
|
88
|
-
return res.redirect('
|
|
87
|
+
return res.redirect(new URL('#/signin?message=login-failed', adminURL));
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
//Accept invite
|
|
@@ -106,7 +105,7 @@ module.exports = function setupOAuthApp() {
|
|
|
106
105
|
|
|
107
106
|
await auth.session.sessionService.createSessionForUser(req, res, req.user);
|
|
108
107
|
|
|
109
|
-
return res.redirect(
|
|
108
|
+
return res.redirect(adminURL);
|
|
110
109
|
}), {
|
|
111
110
|
scope: ['profile', 'email'],
|
|
112
111
|
session: false,
|
|
@@ -133,7 +132,7 @@ module.exports = function setupOAuthApp() {
|
|
|
133
132
|
|
|
134
133
|
oauthApp.get('/:provider/callback', (req, res, next) => {
|
|
135
134
|
// Set the referrer as the ghost instance domain so that the session is linked to the ghost instance domain
|
|
136
|
-
req.headers.referrer = urlUtils.
|
|
135
|
+
req.headers.referrer = urlUtils.getAdminUrl();
|
|
137
136
|
next();
|
|
138
137
|
}, auth.authenticate.authenticateAdminApi, (req, res, next) => {
|
|
139
138
|
if (req.params.provider !== 'google') {
|