ghost 4.41.1 → 4.42.0

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 (82) hide show
  1. package/core/boot.js +24 -6
  2. package/core/built/assets/ghost-dark-97613c037232aba4490489431ce170ca.css +1 -0
  3. package/core/built/assets/{ghost.min-8e2e6c7a01fde044c566c1650a36bfc2.js → ghost.min-20096eef632760c3a2906e243adbd24b.js} +1035 -841
  4. package/core/built/assets/ghost.min-c08ce1872f0e09edb63eb13c43606d18.css +1 -0
  5. package/core/built/assets/{vendor.min-9094db77ba3190cb10876f8e42e1d90d.js → vendor.min-21f79c68a284acb1b70039f3f63e5507.js} +68 -68
  6. package/core/built/assets/{vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css → vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css} +214 -0
  7. package/core/frontend/helpers/price.js +1 -0
  8. package/core/frontend/web/middleware/error-handler.js +5 -3
  9. package/core/server/api/canary/email-preview.js +2 -1
  10. package/core/server/api/canary/{email.js → emails.js} +0 -0
  11. package/core/server/api/canary/index.js +11 -3
  12. package/core/server/api/canary/{memberSigninUrls.js → member-signin-urls.js} +0 -1
  13. package/core/server/api/canary/{membersStripeConnect.js → members-stripe-connect.js} +0 -0
  14. package/core/server/api/canary/members.js +0 -45
  15. package/core/server/api/canary/newsletters.js +45 -0
  16. package/core/server/api/canary/stats.js +14 -0
  17. package/core/server/api/canary/utils/serializers/output/authentication.js +4 -0
  18. package/core/server/api/canary/utils/serializers/output/default.js +35 -0
  19. package/core/server/api/canary/utils/serializers/output/email-previews.js +7 -0
  20. package/core/server/api/canary/utils/serializers/output/index.js +18 -42
  21. package/core/server/api/canary/utils/serializers/output/mappers/authors.js +1 -0
  22. package/core/server/api/canary/utils/serializers/output/mappers/index.js +2 -1
  23. package/core/server/api/canary/utils/serializers/output/mappers/integrations.js +1 -1
  24. package/core/server/api/canary/utils/serializers/output/mappers/snippets.js +36 -0
  25. package/core/server/api/canary/utils/serializers/output/members-stripe-connect.js +6 -0
  26. package/core/server/api/canary/utils/serializers/output/members.js +2 -2
  27. package/core/server/api/canary/utils/serializers/output/oembed.js +2 -2
  28. package/core/server/api/canary/utils/serializers/output/offers.js +8 -0
  29. package/core/server/api/canary/utils/serializers/output/redirects.js +2 -2
  30. package/core/server/api/canary/utils/serializers/output/schedules.js +2 -2
  31. package/core/server/api/canary/utils/serializers/output/session.js +9 -0
  32. package/core/server/api/canary/utils/serializers/output/settings.js +64 -37
  33. package/core/server/api/canary/utils/serializers/output/slack.js +9 -0
  34. package/core/server/api/canary/utils/serializers/output/themes.js +3 -24
  35. package/core/server/api/canary/utils/serializers/output/users.js +0 -23
  36. package/core/server/api/shared/serializers/handle.js +25 -11
  37. package/core/server/data/exporter/table-lists.js +1 -0
  38. package/core/server/data/migrations/utils.js +1 -1
  39. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +20 -0
  40. package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js +28 -0
  41. package/core/server/data/schema/commands.js +13 -13
  42. package/core/server/data/schema/schema.js +18 -0
  43. package/core/server/models/newsletter.js +9 -0
  44. package/core/server/services/mega/template.js +25 -13
  45. package/core/server/services/members/service.js +2 -1
  46. package/core/server/services/offers/service.js +11 -8
  47. package/core/server/services/stats/index.js +1 -0
  48. package/core/server/services/stats/lib/members-stats-service.js +165 -0
  49. package/core/server/services/stats/service.js +6 -0
  50. package/core/server/services/themes/validate.js +4 -3
  51. package/core/server/services/webhooks/webhooks-service.js +3 -1
  52. package/core/server/web/admin/app.js +8 -0
  53. package/core/server/web/admin/views/default-prod.html +5 -5
  54. package/core/server/web/admin/views/default.html +5 -5
  55. package/core/server/web/api/canary/admin/routes.js +8 -2
  56. package/core/shared/config/defaults.json +2 -2
  57. package/core/shared/config/env/config.development.json +26 -0
  58. package/core/shared/config/env/config.production.json +21 -0
  59. package/core/shared/config/env/config.testing-mysql.json +59 -0
  60. package/core/shared/config/env/config.testing.json +58 -0
  61. package/core/shared/labs.js +3 -1
  62. package/core/shared/settings-cache/cache.js +1 -1
  63. package/package.json +66 -66
  64. package/yarn.lock +1609 -1781
  65. package/.c8rc.json +0 -34
  66. package/.eslintrc.js +0 -118
  67. package/core/built/assets/ghost-dark-6fbe502f2bb2cde92e15b2f1a9da57a0.css +0 -1
  68. package/core/built/assets/ghost.min-09301e5bd933cf6d24368e98a4d898a9.css +0 -1
  69. package/core/server/api/canary/utils/serializers/output/actions.js +0 -13
  70. package/core/server/api/canary/utils/serializers/output/authors.js +0 -21
  71. package/core/server/api/canary/utils/serializers/output/email-preview.js +0 -7
  72. package/core/server/api/canary/utils/serializers/output/emails.js +0 -22
  73. package/core/server/api/canary/utils/serializers/output/identities.js +0 -7
  74. package/core/server/api/canary/utils/serializers/output/integrations.js +0 -34
  75. package/core/server/api/canary/utils/serializers/output/invites.js +0 -24
  76. package/core/server/api/canary/utils/serializers/output/labels.js +0 -25
  77. package/core/server/api/canary/utils/serializers/output/mappers/labels.js +0 -4
  78. package/core/server/api/canary/utils/serializers/output/member-signin_urls.js +0 -7
  79. package/core/server/api/canary/utils/serializers/output/snippets.js +0 -97
  80. package/core/server/api/canary/utils/serializers/output/tags.js +0 -25
  81. package/core/server/api/canary/utils/serializers/output/webhooks.js +0 -15
  82. package/jsconfig.json +0 -13
@@ -9,6 +9,8 @@ module.exports = {
9
9
  read: createSerializer('read', singleMember),
10
10
  edit: createSerializer('edit', singleMember),
11
11
  add: createSerializer('add', singleMember),
12
+ destroy: createSerializer('destroy', passthrough),
13
+
12
14
  editSubscription: createSerializer('editSubscription', singleMember),
13
15
  createSubscription: createSerializer('createSubscription', singleMember),
14
16
  bulkDestroy: createSerializer('bulkDestroy', passthrough),
@@ -18,8 +20,6 @@ module.exports = {
18
20
  importCSV: createSerializer('importCSV', passthrough),
19
21
  memberStats: createSerializer('memberStats', passthrough),
20
22
  mrrStats: createSerializer('mrrStats', passthrough),
21
- subscriberStats: createSerializer('subscriberStats', passthrough),
22
- grossVolumeStats: createSerializer('grossVolumeStats', passthrough),
23
23
  activityFeed: createSerializer('activityFeed', passthrough)
24
24
  };
25
25
 
@@ -1,8 +1,8 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:oembed');
2
2
 
3
3
  module.exports = {
4
- all(res, apiConfig, frame) {
4
+ all(data, apiConfig, frame) {
5
5
  debug('all');
6
- frame.response = res;
6
+ frame.response = data;
7
7
  }
8
8
  };
@@ -0,0 +1,8 @@
1
+ const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:offers');
2
+
3
+ module.exports = {
4
+ all() {
5
+ debug('all');
6
+ // Offers has frame.response already set
7
+ }
8
+ };
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
- download(response, apiConfig, frame) {
3
- frame.response = response;
2
+ all(data, apiConfig, frame) {
3
+ frame.response = data;
4
4
  }
5
5
  };
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
- all(model, apiConfig, frame) {
3
- frame.response = model;
2
+ all(data, apiConfig, frame) {
3
+ frame.response = data;
4
4
  }
5
5
  };
@@ -0,0 +1,9 @@
1
+ const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:session');
2
+
3
+ module.exports = {
4
+ all(data, apiConfig, frame) {
5
+ debug('all');
6
+
7
+ frame.response = data;
8
+ }
9
+ };
@@ -1,65 +1,92 @@
1
1
  const _ = require('lodash');
2
2
  const utils = require('../../index');
3
3
  const mappers = require('./mappers');
4
- const _private = {};
5
4
 
6
5
  /**
7
- * ### Settings Filter
8
6
  * Filters an object based on a given filter object
9
7
  * @private
10
8
  * @param {Object} settings
11
9
  * @param {String} filter
12
10
  * @returns {*}
13
11
  */
14
- _private.settingsFilter = (settings, filter) => {
15
- let filteredGroups = filter ? filter.split(',') : false;
12
+ function settingsFilter(settings, filter) {
13
+ let filteredGroups = filter ? filter.split(',') : [];
16
14
  return _.filter(settings, (setting) => {
17
- if (filteredGroups) {
15
+ if (filteredGroups.length > 0) {
18
16
  return _.includes(filteredGroups, setting.group);
19
17
  }
20
18
 
21
19
  return true;
22
20
  });
23
- };
21
+ }
24
22
 
25
- module.exports = {
26
- browse(models, apiConfig, frame) {
27
- let filteredSettings;
23
+ /**
24
+ * Serialies a settings object into the desired API repsonse format
25
+ *
26
+ * @param {Object} models
27
+ * @param {Object} apiConfig
28
+ * @param {Object} frame
29
+ */
30
+ function serializeSettings(models, apiConfig, frame) {
31
+ let filteredSettings;
28
32
 
29
- // If this is public, we already have the right data, we just need to add an Array wrapper
30
- if (utils.isContentAPI(frame)) {
31
- filteredSettings = models;
32
- } else {
33
- filteredSettings = _.values(_private.settingsFilter(models, frame.options.group));
34
- }
33
+ // If this is public, we already have the right data, we just need to add an Array wrapper
34
+ if (utils.isContentAPI(frame)) {
35
+ filteredSettings = models;
36
+ } else {
37
+ filteredSettings = _.values(settingsFilter(models, frame.options.group));
38
+ }
35
39
 
36
- frame.response = {
37
- settings: mappers.settings(filteredSettings, frame),
38
- meta: {}
39
- };
40
+ frame.response = {
41
+ settings: mappers.settings(filteredSettings, frame),
42
+ meta: {}
43
+ };
40
44
 
41
- if (frame.options.type || frame.options.group) {
42
- frame.response.meta.filters = {};
45
+ if (frame.options.type || frame.options.group) {
46
+ frame.response.meta.filters = {};
43
47
 
44
- if (frame.options.type) {
45
- frame.response.meta.filters.type = frame.options.type;
46
- }
48
+ if (frame.options.type) {
49
+ frame.response.meta.filters.type = frame.options.type;
50
+ }
47
51
 
48
- if (frame.options.group) {
49
- frame.response.meta.filters.group = frame.options.group;
50
- }
52
+ if (frame.options.group) {
53
+ frame.response.meta.filters.group = frame.options.group;
51
54
  }
52
- },
55
+ }
56
+ }
53
57
 
54
- read() {
55
- this.browse(...arguments);
56
- },
58
+ /**
59
+ * This noop results in there being no response body
60
+ *
61
+ * @template Data
62
+ * @param {Data} data
63
+ * @returns Data
64
+ */
65
+ function passthrough(data) {
66
+ return data;
67
+ }
68
+
69
+ /**
70
+ * Returns the data as-is without any further modiications
71
+ *
72
+ * @template Data
73
+ * @param {Data} data
74
+ * @param {Object} apiConfig
75
+ * @param {Object} frame
76
+ */
77
+ function serializeData(data, apiConfig, frame) {
78
+ frame.response = data;
79
+ }
80
+
81
+ module.exports = {
82
+ browse: serializeSettings,
83
+ read: serializeSettings,
84
+ edit: serializeSettings,
57
85
 
58
- edit() {
59
- this.browse(...arguments);
60
- },
86
+ download: serializeData,
87
+ upload: serializeData,
61
88
 
62
- download(bytes, apiConfig, frame) {
63
- frame.response = bytes;
64
- }
89
+ updateMembersEmail: passthrough,
90
+ validateMembersEmailUpdate: passthrough,
91
+ disconnectStripeConnectIntegration: passthrough
65
92
  };
@@ -0,0 +1,9 @@
1
+ const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:slack');
2
+
3
+ module.exports = {
4
+ all(data, apiConfig, frame) {
5
+ debug('all');
6
+
7
+ frame.response = data;
8
+ }
9
+ };
@@ -1,30 +1,9 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:themes');
2
2
 
3
3
  module.exports = {
4
- browse(themes, apiConfig, frame) {
5
- debug('browse');
4
+ all(data, apiConfig, frame) {
5
+ debug('all');
6
6
 
7
- frame.response = themes;
8
- },
9
-
10
- upload() {
11
- debug('upload');
12
- this.browse(...arguments);
13
- },
14
-
15
- install() {
16
- debug('install');
17
- this.browse(...arguments);
18
- },
19
-
20
- activate() {
21
- debug('activate');
22
- this.browse(...arguments);
23
- },
24
-
25
- download(fn, apiConfig, frame) {
26
- debug('download');
27
-
28
- frame.response = fn;
7
+ frame.response = data;
29
8
  }
30
9
  };
@@ -1,34 +1,11 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:users');
2
2
  const tpl = require('@tryghost/tpl');
3
- const mappers = require('./mappers');
4
3
 
5
4
  const messages = {
6
5
  pwdChangedSuccessfully: 'Password changed successfully.'
7
6
  };
8
7
 
9
8
  module.exports = {
10
- browse(models, apiConfig, frame) {
11
- debug('browse');
12
-
13
- frame.response = {
14
- users: models.data.map(model => mappers.users(model, frame)),
15
- meta: models.meta
16
- };
17
- },
18
-
19
- read(model, apiConfig, frame) {
20
- debug('read');
21
-
22
- frame.response = {
23
- users: [mappers.users(model, frame)]
24
- };
25
- },
26
-
27
- edit() {
28
- debug('edit');
29
- this.read(...arguments);
30
- },
31
-
32
9
  destroy(filename, apiConfig, frame) {
33
10
  debug('destroy');
34
11
 
@@ -67,6 +67,19 @@ module.exports.input = (apiConfig, apiSerializers, frame) => {
67
67
  return sequence(tasks);
68
68
  };
69
69
 
70
+ const getBestMatchSerializer = function (apiSerializers, docName, method) {
71
+ if (apiSerializers[docName] && apiSerializers[docName][method]) {
72
+ debug(`Calling ${docName}.${method}`);
73
+ return apiSerializers[docName][method].bind(apiSerializers[docName]);
74
+ } else if (apiSerializers[docName] && apiSerializers[docName].all) {
75
+ debug(`Calling ${docName}.all`);
76
+ return apiSerializers[docName].all.bind(apiSerializers[docName]);
77
+ }
78
+
79
+ debug(`Returning as-is`);
80
+ return false;
81
+ };
82
+
70
83
  /**
71
84
  * @description Shared output serialization handler.
72
85
  *
@@ -101,18 +114,19 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => {
101
114
  });
102
115
  }
103
116
 
104
- if (apiSerializers[apiConfig.docName]) {
105
- if (apiSerializers[apiConfig.docName].all) {
106
- tasks.push(function serializeOptionsShared() {
107
- return apiSerializers[apiConfig.docName].all(response, apiConfig, frame);
108
- });
109
- }
117
+ const customSerializer = getBestMatchSerializer(apiSerializers, apiConfig.docName, apiConfig.method);
118
+ const defaultSerializer = getBestMatchSerializer(apiSerializers, 'default', apiConfig.method);
110
119
 
111
- if (apiSerializers[apiConfig.docName][apiConfig.method]) {
112
- tasks.push(function serializeOptionsShared() {
113
- return apiSerializers[apiConfig.docName][apiConfig.method](response, apiConfig, frame);
114
- });
115
- }
120
+ if (customSerializer) {
121
+ // CASE: custom serializer exists
122
+ tasks.push(function doCustomSerializer() {
123
+ return customSerializer(response, apiConfig, frame);
124
+ });
125
+ } else if (defaultSerializer) {
126
+ // CASE: Fall back to default serializer
127
+ tasks.push(function doDefaultSerializer() {
128
+ return defaultSerializer(response, apiConfig, frame);
129
+ });
116
130
  }
117
131
 
118
132
  if (apiSerializers.all && apiSerializers.all.after) {
@@ -15,6 +15,7 @@ const BACKUP_TABLES = [
15
15
  'members_stripe_customers_subscriptions',
16
16
  'migrations',
17
17
  'migrations_lock',
18
+ 'newsletters',
18
19
  'oauth',
19
20
  'permissions',
20
21
  'permissions_roles',
@@ -178,7 +178,7 @@ function addPermissionToRole(config) {
178
178
  return;
179
179
  }
180
180
 
181
- logging.warn(`Adding permission(${config.permission}) to role(${config.role})`);
181
+ logging.info(`Adding permission(${config.permission}) to role(${config.role})`);
182
182
  await connection('permissions_roles').insert({
183
183
  id: ObjectId().toHexString(),
184
184
  permission_id: permission.id,
@@ -0,0 +1,20 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('newsletters', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ name: {type: 'string', maxlength: 191, nullable: false},
6
+ description: {type: 'string', maxlength: 2000, nullable: true},
7
+ sender_name: {type: 'string', maxlength: 191, nullable: false},
8
+ sender_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
9
+ sender_reply_to: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
10
+ default: {type: 'bool', nullable: false, defaultTo: false},
11
+ status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
12
+ recipient_filter: {
13
+ type: 'text',
14
+ maxlength: 1000000000,
15
+ nullable: false,
16
+ defaultTo: ''
17
+ },
18
+ subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: false},
19
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
20
+ });
@@ -0,0 +1,28 @@
1
+ const {
2
+ addPermissionWithRoles,
3
+ combineTransactionalMigrations
4
+ } = require('../../utils');
5
+
6
+ module.exports = combineTransactionalMigrations(
7
+ addPermissionWithRoles({
8
+ name: 'Browse newsletters',
9
+ action: 'browse',
10
+ object: 'newsletter'
11
+ }, [
12
+ 'Administrator'
13
+ ]),
14
+ addPermissionWithRoles({
15
+ name: 'Add newsletters',
16
+ action: 'add',
17
+ object: 'newsletter'
18
+ }, [
19
+ 'Administrator'
20
+ ]),
21
+ addPermissionWithRoles({
22
+ name: 'Edit newsletters',
23
+ action: 'edit',
24
+ object: 'newsletter'
25
+ }, [
26
+ 'Administrator'
27
+ ])
28
+ );
@@ -80,18 +80,18 @@ function dropColumn(tableName, column, transaction = db.knex) {
80
80
  */
81
81
  async function addUnique(tableName, columns, transaction = db.knex) {
82
82
  try {
83
- logging.info(`Adding unique constraint for: ${columns} in table ${tableName}`);
83
+ logging.info(`Adding unique constraint for '${columns}' in table '${tableName}'`);
84
84
 
85
85
  return await transaction.schema.table(tableName, function (table) {
86
86
  table.unique(columns);
87
87
  });
88
88
  } catch (err) {
89
89
  if (err.code === 'SQLITE_ERROR') {
90
- logging.warn(`Constraint for: ${columns} already exists for table: ${tableName}`);
90
+ logging.warn(`Constraint for '${columns}' already exists for table '${tableName}'`);
91
91
  return;
92
92
  }
93
93
  if (err.code === 'ER_DUP_KEYNAME') {
94
- logging.warn(`Constraint for: ${columns} already exists for table: ${tableName}`);
94
+ logging.warn(`Constraint for '${columns}' already exists for table '${tableName}'`);
95
95
  return;
96
96
  }
97
97
  throw err;
@@ -107,18 +107,18 @@ async function addUnique(tableName, columns, transaction = db.knex) {
107
107
  */
108
108
  async function dropUnique(tableName, columns, transaction = db.knex) {
109
109
  try {
110
- logging.info(`Dropping unique constraint for: ${columns} in table: ${tableName}`);
110
+ logging.info(`Dropping unique constraint for '${columns}' in table '${tableName}'`);
111
111
 
112
112
  return await transaction.schema.table(tableName, function (table) {
113
113
  table.dropUnique(columns);
114
114
  });
115
115
  } catch (err) {
116
116
  if (err.code === 'SQLITE_ERROR') {
117
- logging.warn(`Constraint for: ${columns} does not exist for table: ${tableName}`);
117
+ logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`);
118
118
  return;
119
119
  }
120
120
  if (err.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
121
- logging.warn(`Constraint for: ${columns} does not exist for table: ${tableName}`);
121
+ logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`);
122
122
  return;
123
123
  }
124
124
  throw err;
@@ -164,7 +164,7 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
164
164
  if (DatabaseInfo.isSQLite(transaction)) {
165
165
  const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
166
166
  if (foreignKeyExists) {
167
- logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key already exists`);
167
+ logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - already exists`);
168
168
  return;
169
169
  }
170
170
  }
@@ -195,7 +195,7 @@ async function addForeign({fromTable, fromColumn, toTable, toColumn, cascadeDele
195
195
  }
196
196
  } catch (err) {
197
197
  if (err.code === 'ER_DUP_KEY' || err.code === 'ER_FK_DUP_KEY' || err.code === 'ER_FK_DUP_NAME') {
198
- logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key already exists`);
198
+ logging.warn(`Skipped adding foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - already exists`);
199
199
  return;
200
200
  }
201
201
  throw err;
@@ -216,7 +216,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
216
216
  if (DatabaseInfo.isSQLite(transaction)) {
217
217
  const foreignKeyExists = await hasForeignSQLite({fromTable, fromColumn, toTable, toColumn, transaction});
218
218
  if (!foreignKeyExists) {
219
- logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key does not exist`);
219
+ logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - does not exist`);
220
220
  return;
221
221
  }
222
222
  }
@@ -243,7 +243,7 @@ async function dropForeign({fromTable, fromColumn, toTable, toColumn, transactio
243
243
  }
244
244
  } catch (err) {
245
245
  if (err.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
246
- logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - foreign key does not exist`);
246
+ logging.warn(`Skipped dropping foreign key from ${fromTable}.${fromColumn} to ${toTable}.${toColumn} - does not exist`);
247
247
  return;
248
248
  }
249
249
  throw err;
@@ -280,18 +280,18 @@ async function addPrimaryKey(tableName, columns, transaction = db.knex) {
280
280
  if (DatabaseInfo.isSQLite(transaction)) {
281
281
  const primaryKeyExists = await hasPrimaryKeySQLite(tableName, transaction);
282
282
  if (primaryKeyExists) {
283
- logging.warn(`Primary key constraint for: ${columns} already exists for table: ${tableName}`);
283
+ logging.warn(`Primary key constraint for '${columns}' already exists for table '${tableName}'`);
284
284
  return;
285
285
  }
286
286
  }
287
287
  try {
288
- logging.info(`Adding primary key constraint for: ${columns} in table ${tableName}`);
288
+ logging.info(`Adding primary key constraint for '${columns}' in table '${tableName}'`);
289
289
  return await transaction.schema.table(tableName, function (table) {
290
290
  table.primary(columns);
291
291
  });
292
292
  } catch (err) {
293
293
  if (err.code === 'ER_MULTIPLE_PRI_KEY') {
294
- logging.warn(`Primary key constraint for: ${columns} already exists for table: ${tableName}`);
294
+ logging.warn(`Primary key constraint for '${columns}' already exists for table '${tableName}'`);
295
295
  return;
296
296
  }
297
297
  throw err;
@@ -711,5 +711,23 @@ module.exports = {
711
711
  }
712
712
  },
713
713
  value: {type: 'text', maxlength: 65535, nullable: true}
714
+ },
715
+ newsletters: {
716
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
717
+ name: {type: 'string', maxlength: 191, nullable: false},
718
+ description: {type: 'string', maxlength: 2000, nullable: true},
719
+ sender_name: {type: 'string', maxlength: 191, nullable: false},
720
+ sender_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
721
+ sender_reply_to: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
722
+ default: {type: 'bool', nullable: false, defaultTo: false},
723
+ status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
724
+ recipient_filter: {
725
+ type: 'text',
726
+ maxlength: 1000000000,
727
+ nullable: false,
728
+ defaultTo: ''
729
+ },
730
+ subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: false},
731
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
714
732
  }
715
733
  };
@@ -0,0 +1,9 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const Newsletter = ghostBookshelf.Model.extend({
4
+ tableName: 'newsletters'
5
+ });
6
+
7
+ module.exports = {
8
+ Newsletter: ghostBookshelf.model('Newsletter', Newsletter)
9
+ };
@@ -198,48 +198,55 @@ h5,
198
198
  h6 {
199
199
  margin-top: 0;
200
200
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
201
- line-height: 1.15em;
202
- font-weight: 600;
201
+ line-height: 1.11em;
202
+ font-weight: 700;
203
203
  text-rendering: optimizeLegibility;
204
204
  }
205
205
 
206
206
  h1 {
207
207
  margin: 1.5em 0 0.5em 0;
208
208
  font-size: 42px;
209
- font-weight: 600;
209
+ font-weight: 700;
210
210
  }
211
211
 
212
212
  h2 {
213
213
  margin: 1.5em 0 0.5em 0;
214
214
  font-size: 32px;
215
- line-height: 1.22em;
216
215
  }
217
216
 
218
217
  h3 {
219
218
  margin: 1.5em 0 0.5em 0;
220
219
  font-size: 26px;
221
- line-height: 1.25em;
222
220
  }
223
221
 
224
222
  h4 {
225
223
  margin: 1.8em 0 0.5em 0;
226
224
  font-size: 21px;
227
- line-height: 1.3em;
225
+ line-height: 1.2em;
228
226
  }
229
227
 
230
228
  h5 {
231
229
  margin: 2em 0 0.5em 0;
232
230
  font-size: 19px;
233
- line-height: 1.4em;
231
+ line-height: 1.3em;
234
232
  }
235
233
 
236
234
  h6 {
237
235
  margin: 2em 0 0.5em 0;
238
236
  font-size: 19px;
239
- line-height: 1.4em;
237
+ line-height: 1.3em;
240
238
  font-weight: 700;
241
239
  }
242
240
 
241
+ h1 strong,
242
+ h2 strong,
243
+ h3 strong,
244
+ h4 strong,
245
+ h5 strong,
246
+ h6 strong {
247
+ font-weight: 800;
248
+ }
249
+
243
250
  strong {
244
251
  font-weight: 700;
245
252
  }
@@ -301,6 +308,10 @@ figure blockquote p {
301
308
 
302
309
  .site-info {
303
310
  padding-top: 50px;
311
+ }
312
+
313
+ .site-info-bordered {
314
+ padding-top: 50px;
304
315
  border-bottom: 1px solid #e5eff5;
305
316
  }
306
317
 
@@ -322,11 +333,12 @@ figure blockquote p {
322
333
  padding-bottom: 10px;
323
334
  font-size: 42px;
324
335
  line-height: 1.1em;
325
- font-weight: 600;
336
+ font-weight: 700;
326
337
  text-align: center;
327
338
  }
328
339
  .post-title-serif {
329
340
  font-family: Georgia, serif;
341
+ letter-spacing: -0.01em;
330
342
  }
331
343
  .post-title-left {
332
344
  text-align: left;
@@ -388,7 +400,7 @@ figure blockquote p {
388
400
  font-family: Georgia, serif;
389
401
  font-size: 18px;
390
402
  line-height: 1.5em;
391
- color: #23323D;
403
+ color: #15212A;
392
404
  padding-bottom: 20px;
393
405
  border-bottom: 1px solid #e5eff5;
394
406
  }
@@ -397,7 +409,7 @@ figure blockquote p {
397
409
  max-width: 600px !important;
398
410
  font-size: 17px;
399
411
  line-height: 1.5em;
400
- color: #23323D;
412
+ color: #15212A;
401
413
  padding-bottom: 20px;
402
414
  border-bottom: 1px solid #e5eff5;
403
415
  }
@@ -711,7 +723,7 @@ a[data-flickr-embed] img {
711
723
  }
712
724
 
713
725
  .kg-header-card h3 strong {
714
- font-weight: 600;
726
+ font-weight: 700;
715
727
  }
716
728
 
717
729
  .kg-header-card.kg-size-large h3 {
@@ -1148,7 +1160,7 @@ ${ templateSettings.showBadge ? `
1148
1160
 
1149
1161
  ${ templateSettings.showHeaderIcon || templateSettings.showHeaderTitle ? `
1150
1162
  <tr>
1151
- <td class="site-info" width="100%" align="center">
1163
+ <td class="${templateSettings.showHeaderTitle ? `site-info-bordered` : `site-info`}" width="100%" align="center">
1152
1164
  <table role="presentation" border="0" cellpadding="0" cellspacing="0">
1153
1165
  ${ templateSettings.showHeaderIcon && site.iconUrl ? `
1154
1166
  <tr>