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
@@ -1,379 +1 @@
1
- const _ = require('lodash');
2
- const Promise = require('bluebird');
3
- const fs = require('fs-extra');
4
- const path = require('path');
5
- const os = require('os');
6
- const glob = require('glob');
7
- const uuid = require('uuid');
8
- const {extract} = require('@tryghost/zip');
9
- const {pipeline, sequence} = require('@tryghost/promise');
10
- const i18n = require('../../../shared/i18n');
11
- const logging = require('@tryghost/logging');
12
- const errors = require('@tryghost/errors');
13
- const ImageHandler = require('./handlers/image');
14
- const JSONHandler = require('./handlers/json');
15
- const MarkdownHandler = require('./handlers/markdown');
16
- const ImageImporter = require('./importers/image');
17
- const DataImporter = require('./importers/data');
18
-
19
- // Glob levels
20
- const ROOT_ONLY = 0;
21
-
22
- const ROOT_OR_SINGLE_DIR = 1;
23
- const ALL_DIRS = 2;
24
- let defaults;
25
-
26
- defaults = {
27
- extensions: ['.zip'],
28
- contentTypes: ['application/zip', 'application/x-zip-compressed'],
29
- directories: []
30
- };
31
-
32
- function ImportManager() {
33
- this.importers = [ImageImporter, DataImporter];
34
- this.handlers = [ImageHandler, JSONHandler, MarkdownHandler];
35
- // Keep track of file to cleanup at the end
36
- this.fileToDelete = null;
37
- }
38
-
39
- /**
40
- * A number, or a string containing a number.
41
- * @typedef {Object} ImportData
42
- * @property [Object] data
43
- * @property [Array] images
44
- */
45
-
46
- _.extend(ImportManager.prototype, {
47
- /**
48
- * Get an array of all the file extensions for which we have handlers
49
- * @returns {string[]}
50
- */
51
- getExtensions: function () {
52
- return _.flatten(_.union(_.map(this.handlers, 'extensions'), defaults.extensions));
53
- },
54
- /**
55
- * Get an array of all the mime types for which we have handlers
56
- * @returns {string[]}
57
- */
58
- getContentTypes: function () {
59
- return _.flatten(_.union(_.map(this.handlers, 'contentTypes'), defaults.contentTypes));
60
- },
61
- /**
62
- * Get an array of directories for which we have handlers
63
- * @returns {string[]}
64
- */
65
- getDirectories: function () {
66
- return _.flatten(_.union(_.map(this.handlers, 'directories'), defaults.directories));
67
- },
68
- /**
69
- * Convert items into a glob string
70
- * @param {String[]} items
71
- * @returns {String}
72
- */
73
- getGlobPattern: function (items) {
74
- return '+(' + _.reduce(items, function (memo, ext) {
75
- return memo !== '' ? memo + '|' + ext : ext;
76
- }, '') + ')';
77
- },
78
- /**
79
- * @param {String[]} extensions
80
- * @param {Number} level
81
- * @returns {String}
82
- */
83
- getExtensionGlob: function (extensions, level) {
84
- const prefix = level === ALL_DIRS ? '**/*' :
85
- (level === ROOT_OR_SINGLE_DIR ? '{*/*,*}' : '*');
86
-
87
- return prefix + this.getGlobPattern(extensions);
88
- },
89
- /**
90
- *
91
- * @param {String[]} directories
92
- * @param {Number} level
93
- * @returns {String}
94
- */
95
- getDirectoryGlob: function (directories, level) {
96
- const prefix = level === ALL_DIRS ? '**/' :
97
- (level === ROOT_OR_SINGLE_DIR ? '{*/,}' : '');
98
-
99
- return prefix + this.getGlobPattern(directories);
100
- },
101
- /**
102
- * Remove files after we're done (abstracted into a function for easier testing)
103
- * @returns {Function}
104
- */
105
- cleanUp: function () {
106
- const self = this;
107
-
108
- if (self.fileToDelete === null) {
109
- return;
110
- }
111
-
112
- fs.remove(self.fileToDelete, function (err) {
113
- if (err) {
114
- logging.error(new errors.GhostError({
115
- err: err,
116
- context: i18n.t('errors.data.importer.index.couldNotCleanUpFile.error'),
117
- help: i18n.t('errors.data.importer.index.couldNotCleanUpFile.context')
118
- }));
119
- }
120
-
121
- self.fileToDelete = null;
122
- });
123
- },
124
- /**
125
- * Return true if the given file is a Zip
126
- * @returns Boolean
127
- */
128
- isZip: function (ext) {
129
- return _.includes(defaults.extensions, ext);
130
- },
131
- /**
132
- * Checks the content of a zip folder to see if it is valid.
133
- * Importable content includes any files or directories which the handlers can process
134
- * Importable content must be found either in the root, or inside one base directory
135
- *
136
- * @param {String} directory
137
- * @returns {Promise}
138
- */
139
- isValidZip: function (directory) {
140
- // Globs match content in the root or inside a single directory
141
- const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory});
142
-
143
- const extMatchesAll = glob.sync(
144
- this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
145
- );
146
-
147
- const dirMatches = glob.sync(
148
- this.getDirectoryGlob(this.getDirectories(), ROOT_OR_SINGLE_DIR), {cwd: directory}
149
- );
150
-
151
- const oldRoonMatches = glob.sync(this.getDirectoryGlob(['drafts', 'published', 'deleted'], ROOT_OR_SINGLE_DIR),
152
- {cwd: directory});
153
-
154
- // This is a temporary extra message for the old format roon export which doesn't work with Ghost
155
- if (oldRoonMatches.length > 0) {
156
- throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.unsupportedRoonExport')});
157
- }
158
-
159
- // If this folder contains importable files or a content or images directory
160
- if (extMatchesBase.length > 0 || (dirMatches.length > 0 && extMatchesAll.length > 0)) {
161
- return true;
162
- }
163
-
164
- if (extMatchesAll.length < 1) {
165
- throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.noContentToImport')});
166
- }
167
-
168
- throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.invalidZipStructure')});
169
- },
170
- /**
171
- * Use the extract module to extract the given zip file to a temp directory & return the temp directory path
172
- * @param {String} filePath
173
- * @returns {Promise[]} Files
174
- */
175
- extractZip: function (filePath) {
176
- const tmpDir = path.join(os.tmpdir(), uuid.v4());
177
- this.fileToDelete = tmpDir;
178
-
179
- return extract(filePath, tmpDir).then(function () {
180
- return tmpDir;
181
- });
182
- },
183
- /**
184
- * Use the handler extensions to get a globbing pattern, then use that to fetch all the files from the zip which
185
- * are relevant to the given handler, and return them as a name and path combo
186
- * @param {Object} handler
187
- * @param {String} directory
188
- * @returns [] Files
189
- */
190
- getFilesFromZip: function (handler, directory) {
191
- const globPattern = this.getExtensionGlob(handler.extensions, ALL_DIRS);
192
- return _.map(glob.sync(globPattern, {cwd: directory}), function (file) {
193
- return {name: file, path: path.join(directory, file)};
194
- });
195
- },
196
- /**
197
- * Get the name of the single base directory if there is one, else return an empty string
198
- * @param {String} directory
199
- * @returns {Promise (String)}
200
- */
201
- getBaseDirectory: function (directory) {
202
- // Globs match root level only
203
- const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory});
204
-
205
- const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory});
206
- let extMatchesAll;
207
-
208
- // There is no base directory
209
- if (extMatches.length > 0 || dirMatches.length > 0) {
210
- return;
211
- }
212
- // There is a base directory, grab it from any ext match
213
- extMatchesAll = glob.sync(
214
- this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
215
- );
216
- if (extMatchesAll.length < 1 || extMatchesAll[0].split('/') < 1) {
217
- throw new errors.ValidationError({message: i18n.t('errors.data.importer.index.invalidZipFileBaseDirectory')});
218
- }
219
-
220
- return extMatchesAll[0].split('/')[0];
221
- },
222
- /**
223
- * Process Zip
224
- * Takes a reference to a zip file, extracts it, sends any relevant files from inside to the right handler, and
225
- * returns an object in the importData format: {data: {}, images: []}
226
- * The data key contains JSON representing any data that should be imported
227
- * The image key contains references to images that will be stored (and where they will be stored)
228
- * @param {File} file
229
- * @returns {Promise(ImportData)}
230
- */
231
- processZip: function (file) {
232
- const self = this;
233
-
234
- return this.extractZip(file.path).then(function (zipDirectory) {
235
- const ops = [];
236
- const importData = {};
237
- let baseDir;
238
-
239
- self.isValidZip(zipDirectory);
240
- baseDir = self.getBaseDirectory(zipDirectory);
241
-
242
- _.each(self.handlers, function (handler) {
243
- if (Object.prototype.hasOwnProperty.call(importData, handler.type)) {
244
- // This limitation is here to reduce the complexity of the importer for now
245
- return Promise.reject(new errors.UnsupportedMediaTypeError({
246
- message: i18n.t('errors.data.importer.index.zipContainsMultipleDataFormats')
247
- }));
248
- }
249
-
250
- const files = self.getFilesFromZip(handler, zipDirectory);
251
-
252
- if (files.length > 0) {
253
- ops.push(function () {
254
- return handler.loadFile(files, baseDir).then(function (data) {
255
- importData[handler.type] = data;
256
- });
257
- });
258
- }
259
- });
260
-
261
- if (ops.length === 0) {
262
- return Promise.reject(new errors.UnsupportedMediaTypeError({
263
- message: i18n.t('errors.data.importer.index.noContentToImport')
264
- }));
265
- }
266
-
267
- return sequence(ops).then(function () {
268
- return importData;
269
- });
270
- });
271
- },
272
- /**
273
- * Process File
274
- * Takes a reference to a single file, sends it to the relevant handler to be loaded and returns an object in the
275
- * importData format: {data: {}, images: []}
276
- * The data key contains JSON representing any data that should be imported
277
- * The image key contains references to images that will be stored (and where they will be stored)
278
- * @param {File} file
279
- * @returns {Promise(ImportData)}
280
- */
281
- processFile: function (file, ext) {
282
- const fileHandler = _.find(this.handlers, function (handler) {
283
- return _.includes(handler.extensions, ext);
284
- });
285
-
286
- return fileHandler.loadFile([_.pick(file, 'name', 'path')]).then(function (loadedData) {
287
- // normalize the returned data
288
- const importData = {};
289
- importData[fileHandler.type] = loadedData;
290
- return importData;
291
- });
292
- },
293
- /**
294
- * Import Step 1:
295
- * Load the given file into usable importData in the format: {data: {}, images: []}, regardless of
296
- * whether the file is a single importable file like a JSON file, or a zip file containing loads of files.
297
- * @param {File} file
298
- * @returns {Promise}
299
- */
300
- loadFile: function (file) {
301
- const self = this;
302
- const ext = path.extname(file.name).toLowerCase();
303
- return this.isZip(ext) ? self.processZip(file) : self.processFile(file, ext);
304
- },
305
- /**
306
- * Import Step 2:
307
- * Pass the prepared importData through the preProcess function of the various importers, so that the importers can
308
- * make any adjustments to the data based on relationships between it
309
- * @param {ImportData} importData
310
- * @returns {Promise(ImportData)}
311
- */
312
- preProcess: function (importData) {
313
- const ops = [];
314
- _.each(this.importers, function (importer) {
315
- ops.push(function () {
316
- return importer.preProcess(importData);
317
- });
318
- });
319
-
320
- return pipeline(ops);
321
- },
322
- /**
323
- * Import Step 3:
324
- * Each importer gets passed the data from importData which has the key matching its type - i.e. it only gets the
325
- * data that it should import. Each importer then handles actually importing that data into Ghost
326
- * @param {ImportData} importData
327
- * @param {importOptions} importOptions to allow override of certain import features such as locking a user
328
- * @returns {Promise(ImportData)}
329
- */
330
- doImport: function (importData, importOptions) {
331
- importOptions = importOptions || {};
332
- const ops = [];
333
- _.each(this.importers, function (importer) {
334
- if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
335
- ops.push(function () {
336
- return importer.doImport(importData[importer.type], importOptions);
337
- });
338
- }
339
- });
340
-
341
- return sequence(ops).then(function (importResult) {
342
- return importResult;
343
- });
344
- },
345
- /**
346
- * Import Step 4:
347
- * Report on what was imported, currently a no-op
348
- * @param {ImportData} importData
349
- * @returns {Promise(ImportData)}
350
- */
351
- generateReport: function (importData) {
352
- return Promise.resolve(importData);
353
- },
354
- /**
355
- * Import From File
356
- * The main method of the ImportManager, call this to kick everything off!
357
- * @param {File} file
358
- * @param {importOptions} importOptions to allow override of certain import features such as locking a user
359
- * @returns {Promise}
360
- */
361
- importFromFile: function (file, importOptions = {}) {
362
- const self = this;
363
-
364
- // Step 1: Handle converting the file to usable data
365
- return this.loadFile(file).then(function (importData) {
366
- // Step 2: Let the importers pre-process the data
367
- return self.preProcess(importData);
368
- }).then(function (importData) {
369
- // Step 3: Actually do the import
370
- // @TODO: It would be cool to have some sort of dry run flag here
371
- return self.doImport(importData, importOptions);
372
- }).then(function (importData) {
373
- // Step 4: Report on the import
374
- return self.generateReport(importData);
375
- }).finally(() => self.cleanUp()); // Step 5: Cleanup any files
376
- }
377
- });
378
-
379
- module.exports = new ImportManager();
1
+ module.exports = require('./import-manager');
@@ -0,0 +1,9 @@
1
+ const utils = require('../../utils');
2
+
3
+ module.exports = utils.addTable('custom_theme_settings', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ theme: {type: 'string', maxlength: 191, nullable: false},
6
+ key: {type: 'string', maxlength: 191, nullable: false},
7
+ type: {type: 'string', maxlength: 50, nullable: false},
8
+ value: {type: 'text', maxlength: 65535, nullable: true}
9
+ });
@@ -0,0 +1,21 @@
1
+ const {
2
+ addPermissionWithRoles,
3
+ combineTransactionalMigrations
4
+ } = require('../../utils');
5
+
6
+ module.exports = combineTransactionalMigrations(
7
+ addPermissionWithRoles({
8
+ name: 'Browse custom theme settings',
9
+ action: 'browse',
10
+ object: 'custom_theme_setting'
11
+ }, [
12
+ 'Administrator'
13
+ ]),
14
+ addPermissionWithRoles({
15
+ name: 'Edit custom theme settings',
16
+ action: 'edit',
17
+ object: 'custom_theme_setting'
18
+ }, [
19
+ 'Administrator'
20
+ ])
21
+ );
@@ -0,0 +1,19 @@
1
+ const utils = require('../../utils');
2
+
3
+ module.exports = utils.addTable('offers', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ name: {type: 'string', maxlength: 191, nullable: false, unique: true},
6
+ code: {type: 'string', maxlength: 191, nullable: false, unique: true},
7
+ product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id'},
8
+ stripe_coupon_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
9
+ interval: {type: 'string', maxlength: 50, nullable: false},
10
+ currency: {type: 'string', maxlength: 50, nullable: true},
11
+ discount_type: {type: 'string', maxlength: 50, nullable: false},
12
+ discount_amount: {type: 'integer', nullable: false},
13
+ duration: {type: 'string', maxlength: 50, nullable: false},
14
+ duration_in_months: {type: 'integer', nullable: true},
15
+ portal_title: {type: 'string', maxlength: 191, nullable: false},
16
+ portal_description: {type: 'string', maxlength: 2000, nullable: true},
17
+ created_at: {type: 'dateTime', nullable: false},
18
+ updated_at: {type: 'dateTime', nullable: true}
19
+ });
@@ -0,0 +1,35 @@
1
+ const {
2
+ addPermissionWithRoles,
3
+ combineTransactionalMigrations
4
+ } = require('../../utils');
5
+
6
+ module.exports = combineTransactionalMigrations(
7
+ addPermissionWithRoles({
8
+ name: 'Browse offers',
9
+ action: 'browse',
10
+ object: 'offer'
11
+ }, [
12
+ 'Administrator'
13
+ ]),
14
+ addPermissionWithRoles({
15
+ name: 'Read offers',
16
+ action: 'read',
17
+ object: 'offer'
18
+ }, [
19
+ 'Administrator'
20
+ ]),
21
+ addPermissionWithRoles({
22
+ name: 'Edit offers',
23
+ action: 'edit',
24
+ object: 'offer'
25
+ }, [
26
+ 'Administrator'
27
+ ]),
28
+ addPermissionWithRoles({
29
+ name: 'Add offers',
30
+ action: 'add',
31
+ object: 'offer'
32
+ }, [
33
+ 'Administrator'
34
+ ])
35
+ );
@@ -487,10 +487,40 @@
487
487
  "action_type": "destroy",
488
488
  "object_type": "snippet"
489
489
  },
490
+ {
491
+ "name": "Browse offers",
492
+ "action_type": "browse",
493
+ "object_type": "offer"
494
+ },
495
+ {
496
+ "name": "Read offers",
497
+ "action_type": "read",
498
+ "object_type": "offer"
499
+ },
500
+ {
501
+ "name": "Edit offers",
502
+ "action_type": "edit",
503
+ "object_type": "offer"
504
+ },
505
+ {
506
+ "name": "Add offers",
507
+ "action_type": "add",
508
+ "object_type": "offer"
509
+ },
490
510
  {
491
511
  "name": "Reset all passwords",
492
512
  "action_type": "resetAllPasswords",
493
513
  "object_type": "authentication"
514
+ },
515
+ {
516
+ "name": "Browse custom theme settings",
517
+ "action_type": "browse",
518
+ "object_type": "custom_theme_setting"
519
+ },
520
+ {
521
+ "name": "Edit custom theme settings",
522
+ "action_type": "edit",
523
+ "object_type": "custom_theme_setting"
494
524
  }
495
525
  ]
496
526
  },
@@ -738,6 +768,8 @@
738
768
  "email": "all",
739
769
  "member_signin_url": "read",
740
770
  "snippet": "all",
771
+ "custom_theme_setting": "all",
772
+ "offer": "all",
741
773
  "authentication": "resetAllPasswords",
742
774
  "members_stripe_connect": "auth"
743
775
  },
@@ -384,6 +384,23 @@ module.exports = {
384
384
  created_at: {type: 'dateTime', nullable: false},
385
385
  updated_at: {type: 'dateTime', nullable: true}
386
386
  },
387
+ offers: {
388
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
389
+ name: {type: 'string', maxlength: 191, nullable: false, unique: true},
390
+ code: {type: 'string', maxlength: 191, nullable: false, unique: true},
391
+ product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id'},
392
+ stripe_coupon_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
393
+ interval: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['month', 'year']]}},
394
+ currency: {type: 'string', maxlength: 50, nullable: true},
395
+ discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount']]}},
396
+ discount_amount: {type: 'integer', nullable: false},
397
+ duration: {type: 'string', maxlength: 50, nullable: false},
398
+ duration_in_months: {type: 'integer', nullable: true},
399
+ portal_title: {type: 'string', maxlength: 191, nullable: false},
400
+ portal_description: {type: 'string', maxlength: 2000, nullable: true},
401
+ created_at: {type: 'dateTime', nullable: false},
402
+ updated_at: {type: 'dateTime', nullable: true}
403
+ },
387
404
  benefits: {
388
405
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
389
406
  name: {type: 'string', maxlength: 191, nullable: false},
@@ -643,5 +660,21 @@ module.exports = {
643
660
  entry_id: {type: 'string', maxlength: 24, nullable: true},
644
661
  source_url: {type: 'string', maxlength: 2000, nullable: true},
645
662
  metadata: {type: 'string', maxlength: 191, nullable: true}
663
+ },
664
+ custom_theme_settings: {
665
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
666
+ theme: {type: 'string', maxlength: 191, nullable: false},
667
+ key: {type: 'string', maxlength: 191, nullable: false},
668
+ type: {
669
+ type: 'string',
670
+ maxlength: 50,
671
+ nullable: false,
672
+ validations: {
673
+ isIn: [[
674
+ 'select'
675
+ ]]
676
+ }
677
+ },
678
+ value: {type: 'text', maxlength: 65535, nullable: true}
646
679
  }
647
680
  };
@@ -0,0 +1,9 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const CustomThemeSetting = ghostBookshelf.Model.extend({
4
+ tableName: 'custom_theme_settings'
5
+ });
6
+
7
+ module.exports = {
8
+ CustomThemeSetting: ghostBookshelf.model('CustomThemeSetting', CustomThemeSetting)
9
+ };
@@ -17,6 +17,7 @@ const models = [
17
17
  'post',
18
18
  'role',
19
19
  'settings',
20
+ 'custom-theme-setting',
20
21
  'session',
21
22
  'tag',
22
23
  'tag-public',
@@ -39,6 +40,7 @@ const models = [
39
40
  'member-payment-event',
40
41
  'member-status-event',
41
42
  'member-product-event',
43
+ 'member-analytic-event',
42
44
  'posts-meta',
43
45
  'member-stripe-customer',
44
46
  'stripe-customer-subscription',
@@ -0,0 +1,8 @@
1
+ const {Service: CustomThemeSettingsService} = require('@tryghost/custom-theme-settings-service');
2
+ const customThemeSettingsCache = require('../../shared/custom-theme-settings-cache');
3
+ const models = require('../models');
4
+
5
+ module.exports = new CustomThemeSettingsService({
6
+ model: models.CustomThemeSetting,
7
+ cache: customThemeSettingsCache
8
+ });
@@ -9,6 +9,7 @@ const subscribeEmail = require('./emails/subscribe');
9
9
  const updateEmail = require('./emails/updateEmail');
10
10
  const SingleUseTokenProvider = require('./SingleUseTokenProvider');
11
11
  const urlUtils = require('../../../shared/url-utils');
12
+ const labsService = require('../../../shared/labs');
12
13
 
13
14
  const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
14
15
 
@@ -176,12 +177,14 @@ function createApiInstance(config) {
176
177
  MemberPaymentEvent: models.MemberPaymentEvent,
177
178
  MemberStatusEvent: models.MemberStatusEvent,
178
179
  MemberProductEvent: models.MemberProductEvent,
180
+ MemberAnalyticEvent: models.MemberAnalyticEvent,
179
181
  StripeProduct: models.StripeProduct,
180
182
  StripePrice: models.StripePrice,
181
183
  Product: models.Product,
182
184
  Settings: models.Settings
183
185
  },
184
- logger: logging
186
+ logger: logging,
187
+ labsService: labsService
185
188
  });
186
189
 
187
190
  return membersApiInstance;
@@ -0,0 +1,15 @@
1
+ const settings = require('./settings');
2
+ const validation = require('./validation');
3
+
4
+ module.exports = {
5
+ loadRedirectsFile: settings.loadRedirectsFile,
6
+ validate: validation.validate,
7
+ /**
8
+ * Methods used in the API
9
+ */
10
+ api: {
11
+ getRedirectsFilePath: settings.getRedirectsFilePath,
12
+ get: settings.get,
13
+ setFromFilePath: settings.setFromFilePath
14
+ }
15
+ };