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.
Files changed (137) hide show
  1. package/.eslintrc.js +7 -1
  2. package/content/themes/casper/assets/built/screen.css +1 -1
  3. package/content/themes/casper/assets/built/screen.css.map +1 -1
  4. package/content/themes/casper/assets/css/screen.css +1 -1
  5. package/content/themes/casper/default.hbs +2 -2
  6. package/content/themes/casper/package.json +1 -1
  7. package/content/themes/casper/page.hbs +28 -26
  8. package/content/themes/casper/partials/post-card.hbs +2 -2
  9. package/content/themes/casper/post.hbs +67 -65
  10. package/content/themes/casper/tag.hbs +2 -2
  11. package/core/boot.js +7 -7
  12. package/core/bridge.js +4 -3
  13. package/core/built/assets/{chunk.3.4b1d9e20e57164ac9c29.js → chunk.3.b80d3e1e6b8556aaff3c.js} +72 -71
  14. package/core/built/assets/ghost-dark-f7bf2dd8d8c702716f75bfa4ccd92df2.css +1 -0
  15. package/core/built/assets/{ghost.min-e35cfee26d942c364166f57f3dcc9e75.js → ghost.min-52a5420ffcea6bf17761b5c59cf020e2.js} +979 -908
  16. package/core/built/assets/ghost.min-741246f42f000c073999a5363434ea2c.css +1 -0
  17. package/core/built/assets/icons/discount-bubble.svg +1 -0
  18. package/core/built/assets/{vendor.min-ca33abc718f21a51327841d58f8875d0.js → vendor.min-1bfc9d56d27508db88ef417deb55f16f.js} +454 -434
  19. package/core/frontend/apps/amp/lib/helpers/amp_analytics.js +2 -2
  20. package/core/frontend/apps/amp/lib/helpers/amp_components.js +2 -1
  21. package/core/frontend/apps/amp/lib/helpers/amp_content.js +5 -1
  22. package/core/frontend/apps/amp/lib/helpers/amp_style.js +1 -1
  23. package/core/frontend/apps/amp/lib/router.js +8 -4
  24. package/core/frontend/apps/private-blogging/index.js +13 -5
  25. package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +1 -1
  26. package/core/frontend/apps/private-blogging/lib/middleware.js +8 -3
  27. package/core/frontend/helpers/asset.js +10 -2
  28. package/core/frontend/helpers/author.js +5 -3
  29. package/core/frontend/helpers/authors.js +4 -3
  30. package/core/frontend/helpers/body_class.js +1 -1
  31. package/core/frontend/helpers/cancel_link.js +9 -2
  32. package/core/frontend/helpers/concat.js +1 -1
  33. package/core/frontend/helpers/content.js +1 -1
  34. package/core/frontend/helpers/date.js +1 -1
  35. package/core/frontend/helpers/encode.js +1 -1
  36. package/core/frontend/helpers/excerpt.js +2 -1
  37. package/core/frontend/helpers/facebook_url.js +2 -1
  38. package/core/frontend/helpers/foreach.js +11 -2
  39. package/core/frontend/helpers/get.js +14 -3
  40. package/core/frontend/helpers/ghost_foot.js +2 -1
  41. package/core/frontend/helpers/ghost_head.js +10 -1
  42. package/core/frontend/helpers/has.js +8 -3
  43. package/core/frontend/helpers/img_url.js +9 -3
  44. package/core/frontend/helpers/is.js +7 -2
  45. package/core/frontend/helpers/lang.js +1 -1
  46. package/core/frontend/helpers/link.js +11 -2
  47. package/core/frontend/helpers/link_class.js +11 -2
  48. package/core/frontend/helpers/match.js +12 -3
  49. package/core/frontend/helpers/navigation.js +13 -4
  50. package/core/frontend/helpers/pagination.js +15 -5
  51. package/core/frontend/helpers/plural.js +8 -2
  52. package/core/frontend/helpers/post_class.js +1 -1
  53. package/core/frontend/helpers/prev_post.js +9 -2
  54. package/core/frontend/helpers/price.js +11 -6
  55. package/core/frontend/helpers/products.js +2 -1
  56. package/core/frontend/helpers/reading_time.js +4 -2
  57. package/core/frontend/helpers/t.js +1 -1
  58. package/core/frontend/helpers/tags.js +3 -1
  59. package/core/frontend/helpers/title.js +1 -1
  60. package/core/frontend/helpers/twitter_url.js +2 -1
  61. package/core/frontend/helpers/url.js +3 -1
  62. package/core/frontend/services/proxy.js +34 -57
  63. package/core/frontend/services/rendering.js +24 -0
  64. package/core/frontend/services/routing/controllers/channel.js +6 -2
  65. package/core/frontend/services/routing/controllers/collection.js +6 -2
  66. package/core/frontend/services/routing/middlewares/page-param.js +6 -2
  67. package/core/frontend/services/theme-engine/middleware.js +23 -6
  68. package/core/frontend/services/theme-engine/preview.js +31 -8
  69. package/core/server/adapters/scheduling/post-scheduling/scheduler-intergation.js +6 -4
  70. package/core/server/adapters/storage/LocalFileStorage.js +10 -4
  71. package/core/server/api/canary/custom-theme-settings.js +22 -0
  72. package/core/server/api/canary/index.js +4 -0
  73. package/core/server/api/canary/members.js +1 -1
  74. package/core/server/api/canary/redirects.js +5 -5
  75. package/core/server/api/canary/settings.js +16 -148
  76. package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +13 -0
  77. package/core/server/api/canary/utils/serializers/output/index.js +4 -0
  78. package/core/server/api/canary/utils/validators/input/settings.js +23 -1
  79. package/core/server/api/v2/redirects.js +3 -3
  80. package/core/server/api/v2/settings.js +3 -4
  81. package/core/server/api/v3/redirects.js +5 -5
  82. package/core/server/api/v3/settings.js +16 -136
  83. package/core/server/api/v3/utils/validators/input/settings.js +23 -1
  84. package/core/server/data/db/state-manager.js +1 -1
  85. package/core/server/data/exporter/table-lists.js +3 -1
  86. package/core/server/data/importer/import-manager.js +398 -0
  87. package/core/server/data/importer/importers/data/data-importer.js +162 -0
  88. package/core/server/data/importer/importers/data/index.js +1 -162
  89. package/core/server/data/importer/index.js +1 -379
  90. package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
  91. package/core/server/data/migrations/versions/4.17/01-add-custom-theme-settings-permissions.js +21 -0
  92. package/core/server/data/migrations/versions/4.17/02-add-offers-table.js +19 -0
  93. package/core/server/data/migrations/versions/4.17/03-add-offers-permissions.js +35 -0
  94. package/core/server/data/schema/fixtures/fixtures.json +32 -0
  95. package/core/server/data/schema/schema.js +33 -0
  96. package/core/server/models/custom-theme-setting.js +9 -0
  97. package/core/server/models/index.js +2 -0
  98. package/core/server/services/custom-theme-settings.js +8 -0
  99. package/core/server/services/members/api.js +4 -1
  100. package/core/server/services/redirects/index.js +15 -0
  101. package/core/{frontend → server}/services/redirects/settings.js +13 -6
  102. package/core/server/services/redirects/validation.js +44 -0
  103. package/core/{frontend/services/settings → server/services/route-settings}/default-routes.yaml +0 -0
  104. package/core/server/services/route-settings/default-settings-manager.js +62 -0
  105. package/core/server/services/route-settings/index.js +32 -1
  106. package/core/server/services/route-settings/route-settings.js +38 -12
  107. package/core/server/services/route-settings/settings-loader.js +102 -0
  108. package/core/{frontend/services/settings → server/services/route-settings}/validate.js +38 -28
  109. package/core/server/services/route-settings/yaml-parser.js +53 -0
  110. package/core/server/services/settings/index.js +13 -16
  111. package/core/server/services/settings/settings-bread-service.js +188 -0
  112. package/core/server/services/settings/settings-utils.js +32 -0
  113. package/core/server/services/themes/ThemeStorage.js +5 -4
  114. package/core/server/services/themes/activation-bridge.js +14 -0
  115. package/core/server/services/themes/validate.js +5 -2
  116. package/core/server/web/admin/views/default-prod.html +4 -4
  117. package/core/server/web/admin/views/default.html +4 -4
  118. package/core/server/web/api/canary/admin/routes.js +5 -1
  119. package/core/server/web/members/app.js +3 -0
  120. package/core/server/web/oauth/app.js +7 -8
  121. package/core/server/web/shared/middlewares/custom-redirects.js +82 -59
  122. package/core/server/web/site/routes.js +2 -2
  123. package/core/shared/config/defaults.json +2 -2
  124. package/core/shared/config/overrides.json +1 -1
  125. package/core/shared/custom-theme-settings-cache.js +3 -0
  126. package/core/shared/i18n/translations/en.json +2 -13
  127. package/core/shared/labs.js +2 -2
  128. package/package.json +42 -41
  129. package/yarn.lock +916 -901
  130. package/core/built/assets/ghost-dark-faf931d90e92535e6c03ca16793cbe7b.css +0 -1
  131. package/core/built/assets/ghost.min-7aa074ad556a8455155ac88ceaca03ab.css +0 -1
  132. package/core/frontend/services/redirects/index.js +0 -9
  133. package/core/frontend/services/redirects/validation.js +0 -28
  134. package/core/frontend/services/settings/ensure-settings.js +0 -47
  135. package/core/frontend/services/settings/index.js +0 -104
  136. package/core/frontend/services/settings/loader.js +0 -89
  137. 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
- // The string returned when a setting is set as write-only
10
- const obfuscatedSetting = '••••••••';
11
-
12
- // The function used to decide whether a setting is write-only
13
- function isSecretSetting(setting) {
14
- return /secret/.test(setting.key);
15
- }
16
-
17
- // The function that obfuscates a write-only setting
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 fileName
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
- * Rename a file / folder
74
+ * Remove a file / folder
75
75
  *
76
- * @param String backupName
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.15%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" />
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-7aa074ad556a8455155ac88ceaca03ab.css" title="light">
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-ca33abc718f21a51327841d58f8875d0.js"></script>
63
- <script src="assets/ghost.min-e35cfee26d942c364166f57f3dcc9e75.js"></script>
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.15%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" />
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-7aa074ad556a8455155ac88ceaca03ab.css" title="light">
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-ca33abc718f21a51327841d58f8875d0.js"></script>
63
- <script src="assets/ghost.min-e35cfee26d942c364166f57f3dcc9e75.js"></script>
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', labs.enabledMiddleware('membersFiltering'), mw.authAdminApi, http(api.members.bulkEdit));
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
- // TODO: use url config instead of the string /ghost
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(urlUtils.getSiteUrl());
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('/ghost/#/staff/?message=oauth-linking-failed');
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('/ghost/#/signin?message=login-failed');
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('/ghost/#/signin?message=login-failed');
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('/ghost/');
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.getSiteUrl();
135
+ req.headers.referrer = urlUtils.getAdminUrl();
137
136
  next();
138
137
  }, auth.authenticate.authenticateAdminApi, (req, res, next) => {
139
138
  if (req.params.provider !== 'google') {