ghost 5.112.0 → 5.113.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 (157) hide show
  1. package/components/tryghost-adapter-cache-redis-5.113.1.tgz +0 -0
  2. package/components/tryghost-adapter-manager-5.113.1.tgz +0 -0
  3. package/components/tryghost-announcement-bar-settings-5.113.1.tgz +0 -0
  4. package/components/{tryghost-api-framework-5.112.0.tgz → tryghost-api-framework-5.113.1.tgz} +0 -0
  5. package/components/{tryghost-api-version-compatibility-service-5.112.0.tgz → tryghost-api-version-compatibility-service-5.113.1.tgz} +0 -0
  6. package/components/{tryghost-audience-feedback-5.112.0.tgz → tryghost-audience-feedback-5.113.1.tgz} +0 -0
  7. package/components/tryghost-bookshelf-repository-5.113.1.tgz +0 -0
  8. package/components/tryghost-bootstrap-socket-5.113.1.tgz +0 -0
  9. package/components/tryghost-captcha-service-5.113.1.tgz +0 -0
  10. package/components/tryghost-constants-5.113.1.tgz +0 -0
  11. package/components/tryghost-custom-fonts-5.113.1.tgz +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.113.1.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.112.0.tgz → tryghost-data-generator-5.113.1.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.113.1.tgz +0 -0
  15. package/components/tryghost-donations-5.113.1.tgz +0 -0
  16. package/components/tryghost-email-addresses-5.113.1.tgz +0 -0
  17. package/components/tryghost-email-analytics-provider-mailgun-5.113.1.tgz +0 -0
  18. package/components/tryghost-email-analytics-service-5.113.1.tgz +0 -0
  19. package/components/{tryghost-email-content-generator-5.112.0.tgz → tryghost-email-content-generator-5.113.1.tgz} +0 -0
  20. package/components/tryghost-email-events-5.113.1.tgz +0 -0
  21. package/components/{tryghost-email-service-5.112.0.tgz → tryghost-email-service-5.113.1.tgz} +0 -0
  22. package/components/{tryghost-email-suppression-list-5.112.0.tgz → tryghost-email-suppression-list-5.113.1.tgz} +0 -0
  23. package/components/{tryghost-express-dynamic-redirects-5.112.0.tgz → tryghost-express-dynamic-redirects-5.113.1.tgz} +0 -0
  24. package/components/tryghost-extract-api-key-5.113.1.tgz +0 -0
  25. package/components/tryghost-ghost-5.113.1.tgz +0 -0
  26. package/components/tryghost-html-to-plaintext-5.113.1.tgz +0 -0
  27. package/components/tryghost-i18n-5.113.1.tgz +0 -0
  28. package/components/tryghost-identity-token-service-5.113.1.tgz +0 -0
  29. package/components/{tryghost-importer-handler-content-files-5.112.0.tgz → tryghost-importer-handler-content-files-5.113.1.tgz} +0 -0
  30. package/components/{tryghost-importer-revue-5.112.0.tgz → tryghost-importer-revue-5.113.1.tgz} +0 -0
  31. package/components/tryghost-in-memory-repository-5.113.1.tgz +0 -0
  32. package/components/tryghost-job-manager-5.113.1.tgz +0 -0
  33. package/components/{tryghost-link-redirects-5.112.0.tgz → tryghost-link-redirects-5.113.1.tgz} +0 -0
  34. package/components/tryghost-link-replacer-5.113.1.tgz +0 -0
  35. package/components/{tryghost-magic-link-5.112.0.tgz → tryghost-magic-link-5.113.1.tgz} +0 -0
  36. package/components/tryghost-mail-events-5.113.1.tgz +0 -0
  37. package/components/tryghost-mailgun-client-5.113.1.tgz +0 -0
  38. package/components/tryghost-member-attribution-5.113.1.tgz +0 -0
  39. package/components/tryghost-member-events-5.113.1.tgz +0 -0
  40. package/components/{tryghost-members-api-5.112.0.tgz → tryghost-members-api-5.113.1.tgz} +0 -0
  41. package/components/tryghost-members-csv-5.113.1.tgz +0 -0
  42. package/components/{tryghost-members-importer-5.112.0.tgz → tryghost-members-importer-5.113.1.tgz} +0 -0
  43. package/components/{tryghost-members-offers-5.112.0.tgz → tryghost-members-offers-5.113.1.tgz} +0 -0
  44. package/components/tryghost-members-payments-5.113.1.tgz +0 -0
  45. package/components/{tryghost-members-ssr-5.112.0.tgz → tryghost-members-ssr-5.113.1.tgz} +0 -0
  46. package/components/{tryghost-members-stripe-service-5.112.0.tgz → tryghost-members-stripe-service-5.113.1.tgz} +0 -0
  47. package/components/tryghost-milestones-5.113.1.tgz +0 -0
  48. package/components/{tryghost-minifier-5.112.0.tgz → tryghost-minifier-5.113.1.tgz} +0 -0
  49. package/components/tryghost-mw-api-version-mismatch-5.113.1.tgz +0 -0
  50. package/components/tryghost-mw-cache-control-5.113.1.tgz +0 -0
  51. package/components/{tryghost-mw-error-handler-5.112.0.tgz → tryghost-mw-error-handler-5.113.1.tgz} +0 -0
  52. package/components/{tryghost-mw-session-from-token-5.112.0.tgz → tryghost-mw-session-from-token-5.113.1.tgz} +0 -0
  53. package/components/tryghost-mw-update-user-last-seen-5.113.1.tgz +0 -0
  54. package/components/tryghost-mw-version-match-5.113.1.tgz +0 -0
  55. package/components/tryghost-mw-vhost-5.113.1.tgz +0 -0
  56. package/components/{tryghost-package-json-5.112.0.tgz → tryghost-package-json-5.113.1.tgz} +0 -0
  57. package/components/tryghost-post-events-5.113.1.tgz +0 -0
  58. package/components/tryghost-post-revisions-5.113.1.tgz +0 -0
  59. package/components/{tryghost-posts-service-5.112.0.tgz → tryghost-posts-service-5.113.1.tgz} +0 -0
  60. package/components/{tryghost-prometheus-metrics-5.112.0.tgz → tryghost-prometheus-metrics-5.113.1.tgz} +0 -0
  61. package/components/tryghost-recommendations-5.113.1.tgz +0 -0
  62. package/components/tryghost-referrers-5.113.1.tgz +0 -0
  63. package/components/{tryghost-security-5.112.0.tgz → tryghost-security-5.113.1.tgz} +0 -0
  64. package/components/tryghost-session-service-5.113.1.tgz +0 -0
  65. package/components/{tryghost-settings-path-manager-5.112.0.tgz → tryghost-settings-path-manager-5.113.1.tgz} +0 -0
  66. package/components/{tryghost-slack-notifications-5.112.0.tgz → tryghost-slack-notifications-5.113.1.tgz} +0 -0
  67. package/components/tryghost-tiers-5.113.1.tgz +0 -0
  68. package/components/tryghost-version-notifications-data-service-5.113.1.tgz +0 -0
  69. package/components/tryghost-webmentions-5.113.1.tgz +0 -0
  70. package/content/themes/casper/LICENSE +1 -1
  71. package/content/themes/casper/README.md +1 -1
  72. package/content/themes/source/LICENSE +1 -1
  73. package/content/themes/source/README.md +1 -1
  74. package/content/themes/source/assets/built/screen.css +1 -1
  75. package/content/themes/source/assets/built/screen.css.map +1 -1
  76. package/content/themes/source/assets/css/screen.css +6 -11
  77. package/content/themes/source/partials/feature-image.hbs +2 -2
  78. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12953 -12243
  79. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ad8698fe.mjs → CodeEditorView-ed5e87be.mjs} +2 -2
  80. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  81. package/core/built/admin/assets/admin-x-settings/{index-463cec50.mjs → index-0ee4d13c.mjs} +2 -2
  82. package/core/built/admin/assets/admin-x-settings/{index-2713e469.mjs → index-9c7da716.mjs} +19975 -19965
  83. package/core/built/admin/assets/admin-x-settings/{modals-033e8fc4.mjs → modals-7708d510.mjs} +2226 -2215
  84. package/core/built/admin/assets/{chunk.524.db49da6fd8ae155205a4.js → chunk.524.7d2b4ca81805244abd69.js} +6 -6
  85. package/core/built/admin/assets/{chunk.582.0bf715eb6807f7641706.js → chunk.582.e10616d33e3b1d681f59.js} +8 -8
  86. package/core/built/admin/assets/{ghost-62bd4d4c837d453e1038808dc1cd1e4c.js → ghost-21b3c99c2e10a2c5c59bafe0b855736e.js} +68 -70
  87. package/core/built/admin/assets/posts/posts.js +1 -1
  88. package/core/built/admin/assets/{vendor-fca15534b8426c0567400113c63a3e21.js → vendor-68a4aa424a179a90f5bbc2b750def576.js} +28 -26
  89. package/core/built/admin/index.html +4 -4
  90. package/core/frontend/services/routing/registry.js +6 -6
  91. package/core/frontend/src/admin-auth/message-handler.js +1 -1
  92. package/core/server/adapters/cache/AdapterCacheMemoryTTL.js +54 -0
  93. package/core/server/adapters/cache/memory-ttl.js +1 -1
  94. package/core/server/data/migrations/versions/5.113/2025-03-07-12-24-00-add-super-editor.js +31 -0
  95. package/core/server/data/migrations/versions/5.113/2025-03-07-12-25-00-add-member-perms-to-super-editor.js +291 -0
  96. package/core/server/data/schema/fixtures/fixtures.json +27 -0
  97. package/core/server/models/invite.js +2 -2
  98. package/core/server/models/role.js +2 -2
  99. package/core/server/models/user.js +39 -28
  100. package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +13 -0
  101. package/core/server/services/email-analytics/lib/queries.js +3 -3
  102. package/core/server/services/media-inliner/ExternalMediaInliner.js +346 -0
  103. package/core/server/services/media-inliner/service.js +1 -1
  104. package/core/server/services/permissions/can-this.js +3 -2
  105. package/core/server/services/url/Resources.js +19 -29
  106. package/core/server/services/url/UrlService.js +2 -12
  107. package/core/server/services/url/Urls.js +17 -33
  108. package/core/shared/config/defaults.json +1 -1
  109. package/core/shared/labs.js +2 -1
  110. package/core/shared/settings-cache/CacheManager.js +4 -4
  111. package/package.json +134 -134
  112. package/yarn.lock +10 -10
  113. package/components/tryghost-adapter-cache-memory-ttl-5.112.0.tgz +0 -0
  114. package/components/tryghost-adapter-cache-redis-5.112.0.tgz +0 -0
  115. package/components/tryghost-adapter-manager-5.112.0.tgz +0 -0
  116. package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
  117. package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
  118. package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
  119. package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
  120. package/components/tryghost-constants-5.112.0.tgz +0 -0
  121. package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
  122. package/components/tryghost-custom-theme-settings-service-5.112.0.tgz +0 -0
  123. package/components/tryghost-domain-events-5.112.0.tgz +0 -0
  124. package/components/tryghost-donations-5.112.0.tgz +0 -0
  125. package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
  126. package/components/tryghost-email-analytics-provider-mailgun-5.112.0.tgz +0 -0
  127. package/components/tryghost-email-analytics-service-5.112.0.tgz +0 -0
  128. package/components/tryghost-email-events-5.112.0.tgz +0 -0
  129. package/components/tryghost-external-media-inliner-5.112.0.tgz +0 -0
  130. package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
  131. package/components/tryghost-ghost-5.112.0.tgz +0 -0
  132. package/components/tryghost-html-to-plaintext-5.112.0.tgz +0 -0
  133. package/components/tryghost-i18n-5.112.0.tgz +0 -0
  134. package/components/tryghost-identity-token-service-5.112.0.tgz +0 -0
  135. package/components/tryghost-in-memory-repository-5.112.0.tgz +0 -0
  136. package/components/tryghost-job-manager-5.112.0.tgz +0 -0
  137. package/components/tryghost-link-replacer-5.112.0.tgz +0 -0
  138. package/components/tryghost-mail-events-5.112.0.tgz +0 -0
  139. package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
  140. package/components/tryghost-member-attribution-5.112.0.tgz +0 -0
  141. package/components/tryghost-member-events-5.112.0.tgz +0 -0
  142. package/components/tryghost-members-csv-5.112.0.tgz +0 -0
  143. package/components/tryghost-members-payments-5.112.0.tgz +0 -0
  144. package/components/tryghost-milestones-5.112.0.tgz +0 -0
  145. package/components/tryghost-mw-api-version-mismatch-5.112.0.tgz +0 -0
  146. package/components/tryghost-mw-cache-control-5.112.0.tgz +0 -0
  147. package/components/tryghost-mw-update-user-last-seen-5.112.0.tgz +0 -0
  148. package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
  149. package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
  150. package/components/tryghost-post-events-5.112.0.tgz +0 -0
  151. package/components/tryghost-post-revisions-5.112.0.tgz +0 -0
  152. package/components/tryghost-recommendations-5.112.0.tgz +0 -0
  153. package/components/tryghost-referrers-5.112.0.tgz +0 -0
  154. package/components/tryghost-session-service-5.112.0.tgz +0 -0
  155. package/components/tryghost-tiers-5.112.0.tgz +0 -0
  156. package/components/tryghost-version-notifications-data-service-5.112.0.tgz +0 -0
  157. package/components/tryghost-webmentions-5.112.0.tgz +0 -0
@@ -0,0 +1,54 @@
1
+ const TTLCache = require('@isaacs/ttlcache');
2
+ const Base = require('@tryghost/adapter-base-cache');
3
+
4
+ /**
5
+ * Cache adapter compatible wrapper around TTLCache
6
+ * Distinct features of this cache adapter:
7
+ * - it is in-memory only
8
+ * - it supports time-to-live (TTL)
9
+ * - it supports a max number of items
10
+ */
11
+ class AdapterCacheMemoryTTL extends Base {
12
+ #cache;
13
+
14
+ /**
15
+ *
16
+ * @param {Object} [deps]
17
+ * @param {Number} [deps.max] - The max number of items to keep in the cache.
18
+ * @param {Number} [deps.ttl] - The max time in ms to store items
19
+ */
20
+ constructor({max = Infinity, ttl = Infinity} = {}) {
21
+ super();
22
+
23
+ this.#cache = new TTLCache({max, ttl});
24
+ }
25
+
26
+ get(key) {
27
+ return this.#cache.get(key);
28
+ }
29
+
30
+ /**
31
+ *
32
+ * @param {String} key
33
+ * @param {*} value
34
+ * @param {Object} [options]
35
+ * @param {Number} [options.ttl]
36
+ */
37
+ set(key, value, {ttl} = {}) {
38
+ this.#cache.set(key, value, {ttl});
39
+ }
40
+
41
+ reset() {
42
+ this.#cache.clear();
43
+ }
44
+
45
+ /**
46
+ * Helper method to assist "getAll" type of operations
47
+ * @returns {Array<String>} all keys present in the cache
48
+ */
49
+ keys() {
50
+ return [...this.#cache.keys()];
51
+ }
52
+ }
53
+
54
+ module.exports = AdapterCacheMemoryTTL;
@@ -1,3 +1,3 @@
1
- const TTLMemoryCache = require('@tryghost/adapter-cache-memory-ttl');
1
+ const TTLMemoryCache = require('./AdapterCacheMemoryTTL');
2
2
 
3
3
  module.exports = TTLMemoryCache;
@@ -0,0 +1,31 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {default: ObjectID} = require('bson-objectid');
3
+ const {createTransactionalMigration, meta} = require('../../utils');
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ logging.info('Creating "Super Editor" role');
8
+ const existingRole = await knex('roles').where({
9
+ name: 'Super Editor'
10
+ }).first();
11
+
12
+ if (existingRole) {
13
+ logging.warn('"Super Editor" role already exists, skipping');
14
+ return;
15
+ }
16
+
17
+ await knex('roles').insert({
18
+ id: (new ObjectID()).toHexString(),
19
+ name: 'Super Editor',
20
+ description: 'Editor plus member management',
21
+ created_by: meta.MIGRATION_USER,
22
+ created_at: knex.raw('current_timestamp')
23
+ });
24
+ },
25
+ async function down(knex) {
26
+ logging.info('Deleting role "Super Editor"');
27
+ await knex('roles').where({
28
+ name: 'Super Editor'
29
+ }).del();
30
+ }
31
+ );
@@ -0,0 +1,291 @@
1
+ const {addPermissionToRole, combineTransactionalMigrations} = require('../../utils');
2
+ module.exports = combineTransactionalMigrations(
3
+ addPermissionToRole({
4
+ permission: 'Browse Members',
5
+ role: 'Super Editor'
6
+ }),
7
+ addPermissionToRole({
8
+ permission: 'Read Members',
9
+ role: 'Super Editor'
10
+ }),
11
+ addPermissionToRole({
12
+ permission: 'Edit Members',
13
+ role: 'Super Editor'
14
+ }),
15
+ addPermissionToRole({
16
+ permission: 'Add Members',
17
+ role: 'Super Editor'
18
+ }),
19
+ addPermissionToRole({
20
+ permission: 'Delete Members',
21
+ role: 'Super Editor'
22
+ }),
23
+ addPermissionToRole({
24
+ permission: 'Read offers',
25
+ role: 'Super Editor'
26
+ }),
27
+ addPermissionToRole({
28
+ permission: 'Browse offers',
29
+ role: 'Super Editor'
30
+ }),
31
+ addPermissionToRole({
32
+ permission: 'Read member signin urls',
33
+ role: 'Super Editor'
34
+ }),
35
+ addPermissionToRole({
36
+ permission: 'Browse notifications',
37
+ role: 'Super Editor'
38
+ }),
39
+ addPermissionToRole({
40
+ permission: 'Add notifications',
41
+ role: 'Super Editor'
42
+ }),
43
+ addPermissionToRole({
44
+ permission: 'Delete notifications',
45
+ role: 'Super Editor'
46
+ }),
47
+ addPermissionToRole({
48
+ permission: 'Generate slugs',
49
+ role: 'Super Editor'
50
+ }),
51
+ addPermissionToRole({
52
+ permission: 'Browse posts',
53
+ role: 'Super Editor'
54
+ }),
55
+ addPermissionToRole({
56
+ permission: 'Read posts',
57
+ role: 'Super Editor'
58
+ }),
59
+ addPermissionToRole({
60
+ permission: 'Edit posts',
61
+ role: 'Super Editor'
62
+ }),
63
+ addPermissionToRole({
64
+ permission: 'Add posts',
65
+ role: 'Super Editor'
66
+ }),
67
+ addPermissionToRole({
68
+ permission: 'Delete posts',
69
+ role: 'Super Editor'
70
+ }),
71
+ addPermissionToRole({
72
+ permission: 'Publish posts',
73
+ role: 'Super Editor'
74
+ }),
75
+ addPermissionToRole({
76
+ permission: 'Browse settings',
77
+ role: 'Super Editor'
78
+ }),
79
+ addPermissionToRole({
80
+ permission: 'Read settings',
81
+ role: 'Super Editor'
82
+ }),
83
+ addPermissionToRole({
84
+ permission: 'Browse tags',
85
+ role: 'Super Editor'
86
+ }),
87
+ addPermissionToRole({
88
+ permission: 'Read tags',
89
+ role: 'Super Editor'
90
+ }),
91
+ addPermissionToRole({
92
+ permission: 'Edit tags',
93
+ role: 'Super Editor'
94
+ }),
95
+ addPermissionToRole({
96
+ permission: 'Add tags',
97
+ role: 'Super Editor'
98
+ }),
99
+ addPermissionToRole({
100
+ permission: 'Delete tags',
101
+ role: 'Super Editor'
102
+ }),
103
+ addPermissionToRole({
104
+ permission: 'Browse themes',
105
+ role: 'Super Editor'
106
+ }),
107
+ addPermissionToRole({
108
+ permission: 'View active theme details',
109
+ role: 'Super Editor'
110
+ }),
111
+ addPermissionToRole({
112
+ permission: 'Browse users',
113
+ role: 'Super Editor'
114
+ }),
115
+ addPermissionToRole({
116
+ permission: 'Read users',
117
+ role: 'Super Editor'
118
+ }),
119
+ addPermissionToRole({
120
+ permission: 'Edit users',
121
+ role: 'Super Editor'
122
+ }),
123
+ addPermissionToRole({
124
+ permission: 'Add users',
125
+ role: 'Super Editor'
126
+ }),
127
+ addPermissionToRole({
128
+ permission: 'Delete users',
129
+ role: 'Super Editor'
130
+ }),
131
+ addPermissionToRole({
132
+ permission: 'Assign a role',
133
+ role: 'Super Editor'
134
+ }),
135
+ addPermissionToRole({
136
+ permission: 'Browse roles',
137
+ role: 'Super Editor'
138
+ }),
139
+ addPermissionToRole({
140
+ permission: 'Browse invites',
141
+ role: 'Super Editor'
142
+ }),
143
+ addPermissionToRole({
144
+ permission: 'Read invites',
145
+ role: 'Super Editor'
146
+ }),
147
+ addPermissionToRole({
148
+ permission: 'Add invites',
149
+ role: 'Super Editor'
150
+ }),
151
+ addPermissionToRole({
152
+ permission: 'Delete invites',
153
+ role: 'Super Editor'
154
+ }),
155
+ addPermissionToRole({
156
+ permission: 'Edit invites',
157
+ role: 'Super Editor'
158
+ }),
159
+ addPermissionToRole({
160
+ permission: 'Email preview',
161
+ role: 'Super Editor'
162
+ }),
163
+ addPermissionToRole({
164
+ permission: 'Send test email',
165
+ role: 'Super Editor'
166
+ }),
167
+ addPermissionToRole({
168
+ permission: 'Read emails',
169
+ role: 'Super Editor'
170
+ }),
171
+ addPermissionToRole({
172
+ permission: 'Browse emails',
173
+ role: 'Super Editor'
174
+ }),
175
+ addPermissionToRole({
176
+ permission: 'Retry emails',
177
+ role: 'Super Editor'
178
+ }),
179
+ addPermissionToRole({
180
+ permission: 'Browse snippets',
181
+ role: 'Super Editor'
182
+ }),
183
+ addPermissionToRole({
184
+ permission: 'Read snippets',
185
+ role: 'Super Editor'
186
+ }),
187
+ addPermissionToRole({
188
+ permission: 'Edit snippets',
189
+ role: 'Super Editor'
190
+ }),
191
+ addPermissionToRole({
192
+ permission: 'Add snippets',
193
+ role: 'Super Editor'
194
+ }),
195
+ addPermissionToRole({
196
+ permission: 'Delete snippets',
197
+ role: 'Super Editor'
198
+ }),
199
+ addPermissionToRole({
200
+ permission: 'Browse labels',
201
+ role: 'Super Editor'
202
+ }),
203
+ addPermissionToRole({
204
+ permission: 'Read labels',
205
+ role: 'Super Editor'
206
+ }),
207
+ addPermissionToRole({
208
+ permission: 'Edit labels',
209
+ role: 'Super Editor'
210
+ }),
211
+ addPermissionToRole({
212
+ permission: 'Add labels',
213
+ role: 'Super Editor'
214
+ }),
215
+ addPermissionToRole({
216
+ permission: 'Delete labels',
217
+ role: 'Super Editor'
218
+ }),
219
+ addPermissionToRole({
220
+ permission: 'Browse Products',
221
+ role: 'Super Editor'
222
+ }),
223
+ addPermissionToRole({
224
+ permission: 'Read Products',
225
+ role: 'Super Editor'
226
+ }),
227
+ addPermissionToRole({
228
+ permission: 'Browse newsletters',
229
+ role: 'Super Editor'
230
+ }),
231
+ addPermissionToRole({
232
+ permission: 'Read newsletters',
233
+ role: 'Super Editor'
234
+ }),
235
+ addPermissionToRole({
236
+ permission: 'Browse collections',
237
+ role: 'Super Editor'
238
+ }),
239
+ addPermissionToRole({
240
+ permission: 'Read collections',
241
+ role: 'Super Editor'
242
+ }),
243
+ addPermissionToRole({
244
+ permission: 'Edit collections',
245
+ role: 'Super Editor'
246
+ }),
247
+ addPermissionToRole({
248
+ permission: 'Add collections',
249
+ role: 'Super Editor'
250
+ }),
251
+ addPermissionToRole({
252
+ permission: 'Delete collections',
253
+ role: 'Super Editor'
254
+ }),
255
+ addPermissionToRole({
256
+ permission: 'Moderate comments',
257
+ role: 'Super Editor'
258
+ }),
259
+ addPermissionToRole({
260
+ permission: 'Like comments',
261
+ role: 'Super Editor'
262
+ }),
263
+ addPermissionToRole({
264
+ permission: 'Unlike comments',
265
+ role: 'Super Editor'
266
+ }),
267
+ addPermissionToRole({
268
+ permission: 'Add comments',
269
+ role: 'Super Editor'
270
+ }),
271
+ addPermissionToRole({
272
+ permission: 'Edit comments',
273
+ role: 'Super Editor'
274
+ }),
275
+ addPermissionToRole({
276
+ permission: 'Delete comments',
277
+ role: 'Super Editor'
278
+ }),
279
+ addPermissionToRole({
280
+ permission: 'Read comments',
281
+ role: 'Super Editor'
282
+ }),
283
+ addPermissionToRole({
284
+ permission: 'Browse comments',
285
+ role: 'Super Editor'
286
+ }),
287
+ addPermissionToRole({
288
+ permission: 'Report comments',
289
+ role: 'Super Editor'
290
+ })
291
+ );
@@ -106,6 +106,10 @@
106
106
  {
107
107
  "name": "Scheduler Integration",
108
108
  "description": "Internal Scheduler Client"
109
+ },
110
+ {
111
+ "name": "Super Editor",
112
+ "description": "Super Editors"
109
113
  }
110
114
  ]
111
115
  },
@@ -917,6 +921,29 @@
917
921
  "recommendation": "all",
918
922
  "member_signin_url": "read"
919
923
  },
924
+ "Super Editor": {
925
+ "notification": "all",
926
+ "post": "all",
927
+ "setting": ["browse", "read"],
928
+ "slug": "all",
929
+ "tag": "all",
930
+ "user": "all",
931
+ "role": "all",
932
+ "invite": "all",
933
+ "theme": ["browse", "readActive"],
934
+ "email_preview": "all",
935
+ "email": "all",
936
+ "snippet": "all",
937
+ "label": ["browse", "read", "edit", "add", "destroy"],
938
+ "product": ["browse", "read"],
939
+ "newsletter": ["browse", "read"],
940
+ "collection": "all",
941
+ "recommendation": ["browse", "read"],
942
+ "member": ["browse", "read", "add", "edit", "destroy"],
943
+ "member_signin_url": "read",
944
+ "offer": ["browse", "read"],
945
+ "comment": "all"
946
+ },
920
947
  "Editor": {
921
948
  "notification": "all",
922
949
  "post": "all",
@@ -87,12 +87,12 @@ Invite = ghostBookshelf.Model.extend({
87
87
  if (loadedPermissions.user) {
88
88
  const {isOwner, isAdmin, isEitherEditor} = setIsRoles(loadedPermissions);
89
89
  if (isOwner || isAdmin) {
90
- allowed = ['Administrator', 'Editor', 'Author', 'Contributor'];
90
+ allowed = ['Administrator', 'Editor', 'Author', 'Contributor', 'Super Editor'];
91
91
  } else if (isEitherEditor) {
92
92
  allowed = ['Author', 'Contributor'];
93
93
  }
94
94
  } else if (loadedPermissions.apiKey) {
95
- allowed = ['Editor', 'Author', 'Contributor'];
95
+ allowed = ['Editor', 'Author', 'Contributor', 'Super Editor'];
96
96
  }
97
97
 
98
98
  if (allowed.indexOf(roleToInvite.get('name')) === -1) {
@@ -82,9 +82,9 @@ Role = ghostBookshelf.Model.extend({
82
82
  const {isOwner, isAdmin, isEitherEditor} = setIsRoles(loadedPermissions);
83
83
  let checkAgainst;
84
84
  if (isOwner) {
85
- checkAgainst = ['Owner', 'Administrator', 'Editor', 'Author', 'Contributor'];
85
+ checkAgainst = ['Owner', 'Administrator', 'Super Editor', 'Editor', 'Author', 'Contributor'];
86
86
  } else if (isAdmin) {
87
- checkAgainst = ['Administrator', 'Editor', 'Author', 'Contributor'];
87
+ checkAgainst = ['Administrator', 'Super Editor', 'Editor', 'Author', 'Contributor'];
88
88
  } else if (isEitherEditor) {
89
89
  checkAgainst = ['Author', 'Contributor'];
90
90
  }
@@ -1,4 +1,3 @@
1
- const _ = require('lodash');
2
1
  const validator = require('@tryghost/validator');
3
2
  const ObjectId = require('bson-objectid').default;
4
3
  const ghostBookshelf = require('./base');
@@ -11,9 +10,9 @@ const {pipeline} = require('@tryghost/promise');
11
10
  const validatePassword = require('../lib/validate-password');
12
11
  const permissions = require('../services/permissions');
13
12
  const urlUtils = require('../../shared/url-utils');
14
- const activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'];
15
- const ASSIGNABLE_ROLES = ['Administrator', 'Editor', 'Author', 'Contributor'];
16
13
  const {setIsRoles} = require('./role-utils');
14
+ const activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'];
15
+ const ASSIGNABLE_ROLES = ['Administrator', 'Super Editor', 'Editor', 'Author', 'Contributor'];
17
16
 
18
17
  const messages = {
19
18
  valueCannotBeBlank: 'Value in [{tableName}.{columnKey}] cannot be blank.',
@@ -77,7 +76,7 @@ User = ghostBookshelf.Model.extend({
77
76
  },
78
77
 
79
78
  format(options) {
80
- if (!_.isEmpty(options.website) &&
79
+ if (options.website &&
81
80
  !validator.isURL(options.website, {
82
81
  require_protocol: true,
83
82
  protocols: ['http', 'https']
@@ -130,7 +129,7 @@ User = ghostBookshelf.Model.extend({
130
129
  onDestroyed: function onDestroyed(model, options) {
131
130
  ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);
132
131
 
133
- if (_.includes(activeStates, model.previous('status'))) {
132
+ if (activeStates.includes(model.previous('status'))) {
134
133
  model.emitChange('deactivated', options);
135
134
  }
136
135
 
@@ -143,7 +142,7 @@ User = ghostBookshelf.Model.extend({
143
142
  model.emitChange('added', options);
144
143
 
145
144
  // active is the default state, so if status isn't provided, this will be an active user
146
- if (!model.get('status') || _.includes(activeStates, model.get('status'))) {
145
+ if (!model.get('status') || activeStates.includes(model.get('status'))) {
147
146
  model.emitChange('activated', options);
148
147
  }
149
148
  },
@@ -152,7 +151,7 @@ User = ghostBookshelf.Model.extend({
152
151
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
153
152
 
154
153
  model.statusChanging = model.get('status') !== model.previous('status');
155
- model.isActive = _.includes(activeStates, model.get('status'));
154
+ model.isActive = activeStates.includes(model.get('status'));
156
155
 
157
156
  if (model.statusChanging) {
158
157
  model.emitChange(model.isActive ? 'activated' : 'deactivated', options);
@@ -456,18 +455,16 @@ User = ghostBookshelf.Model.extend({
456
455
  const options = this.filterOptions(unfilteredOptions, 'findOne');
457
456
  let query;
458
457
  let status;
459
- let data = _.cloneDeep(dataToClone);
458
+ let data = JSON.parse(JSON.stringify(dataToClone));
460
459
  const lookupRole = data.role;
461
460
 
462
461
  // Ensure only valid fields/columns are added to query
463
462
  if (options.columns) {
464
- options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
463
+ options.columns = options.columns.filter(col => this.prototype.permittedAttributes().includes(col));
465
464
  }
466
465
 
467
466
  delete data.role;
468
- data = _.defaults(data || {}, {
469
- status: 'all'
470
- });
467
+ data = Object.assign({}, {status: 'all'}, data || {});
471
468
 
472
469
  status = data.status;
473
470
  delete data.status;
@@ -476,7 +473,7 @@ User = ghostBookshelf.Model.extend({
476
473
 
477
474
  // Support finding by role
478
475
  if (lookupRole) {
479
- options.withRelated = _.union(options.withRelated, ['roles']);
476
+ options.withRelated = [...new Set([...(options.withRelated || []), 'roles'])];
480
477
  query = this.forge(data);
481
478
 
482
479
  query.query('join', 'roles_users', 'users.id', '=', 'roles_users.user_id');
@@ -520,7 +517,7 @@ User = ghostBookshelf.Model.extend({
520
517
  } else if (type === 'recommendation-received') {
521
518
  filter += '+recommendation_notifications:true';
522
519
  }
523
- const updatedOptions = _.merge({}, options, {filter, withRelated: ['roles']});
520
+ const updatedOptions = Object.assign({}, options, {filter, withRelated: ['roles']});
524
521
  return this.findAll(updatedOptions).then((users) => {
525
522
  return users.toJSON().filter((user) => {
526
523
  return user?.roles?.some((role) => {
@@ -641,7 +638,7 @@ User = ghostBookshelf.Model.extend({
641
638
  add: function add(dataToClone, unfilteredOptions) {
642
639
  const options = this.filterOptions(unfilteredOptions, 'add');
643
640
  const self = this;
644
- const data = _.cloneDeep(dataToClone);
641
+ const data = JSON.parse(JSON.stringify(dataToClone));
645
642
  let userData = this.filterData(data);
646
643
  let roles;
647
644
 
@@ -653,7 +650,7 @@ User = ghostBookshelf.Model.extend({
653
650
  }
654
651
 
655
652
  function getAuthorRole() {
656
- return ghostBookshelf.model('Role').findOne({name: 'Author'}, _.pick(options, 'transacting'))
653
+ return ghostBookshelf.model('Role').findOne({name: 'Author'}, {transacting: options.transacting})
657
654
  .then(function then(authorRole) {
658
655
  return [authorRole.get('id')];
659
656
  });
@@ -684,7 +681,7 @@ User = ghostBookshelf.Model.extend({
684
681
  roles = _roles;
685
682
 
686
683
  // CASE: it is possible to add roles by name, by id or by object
687
- if (_.isString(roles[0]) && !ObjectId.isValid(roles[0])) {
684
+ if (typeof roles[0] === 'string' && !ObjectId.isValid(roles[0])) {
688
685
  const rolePromises = roles.map((roleName) => {
689
686
  return ghostBookshelf.model('Role').findOne({
690
687
  name: roleName
@@ -781,6 +778,23 @@ User = ghostBookshelf.Model.extend({
781
778
  });
782
779
  },
783
780
 
781
+ /**
782
+ * Checks if a user has permission to perform an action on another user
783
+ *
784
+ * @param {Object|string|number} userModelOrId - The user model or ID being acted upon
785
+ * @param {'edit'|'destroy'} action - The action being performed:
786
+ * - 'edit': Edit user details, status, or role
787
+ * - 'destroy': Delete a user (Owner cannot be deleted)
788
+ * @param {Object} context - The context of the request, containing the current user's ID
789
+ * @param {Object} unsafeAttrs - The attributes being modified in the action
790
+ * @param {Object} loadedPermissions - The permissions of the user making the request
791
+ * @param {boolean} hasUserPermission - Whether the user has permission based on user roles
792
+ * @param {boolean} hasApiKeyPermission - Whether the user has permission based on API key
793
+ * @returns {Promise<boolean>} Resolves if the action is permitted, rejects with NoPermissionError if not
794
+ * @throws {errors.NotFoundError} When the target user is not found
795
+ * @throws {errors.NoPermissionError} When the action is not permitted
796
+ * @throws {errors.ValidationError} When role changes are invalid
797
+ */
784
798
  permissible: async function permissible(userModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
785
799
  const self = this;
786
800
  const userModel = userModelOrId;
@@ -788,13 +802,13 @@ User = ghostBookshelf.Model.extend({
788
802
  const {isOwner, isEitherEditor} = setIsRoles(loadedPermissions);
789
803
 
790
804
  // If we passed in a model without its related roles, we need to fetch it again
791
- if (_.isObject(userModelOrId) && !_.isObject(userModelOrId.related('roles'))) {
805
+ if (typeof userModelOrId === 'object' && !(typeof userModelOrId.related('roles') === 'object')) {
792
806
  userModelOrId = userModelOrId.id;
793
807
  }
794
808
  // If we passed in an id instead of a model get the model first
795
- if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) {
809
+ if (typeof userModelOrId === 'number' || typeof userModelOrId === 'string') {
796
810
  // Grab the original args without the first one
797
- origArgs = _.toArray(arguments).slice(1);
811
+ origArgs = Array.from(arguments).slice(1);
798
812
 
799
813
  // Get the actual user model
800
814
  return this.findOne({
@@ -822,15 +836,12 @@ User = ghostBookshelf.Model.extend({
822
836
  }
823
837
 
824
838
  if (action === 'edit') {
825
- // Users with the role 'Editor', 'Author', and 'Contributor' have complex permissions when the action === 'edit'
826
- // We now have all the info we need to construct the permissions
827
-
828
839
  if (context.user === userModel.get('id')) {
829
840
  // If this is the same user that requests the operation allow it.
830
841
  hasUserPermission = true;
831
- } else if (isOwner) {
842
+ } else if (loadedPermissions.user && userModel.hasRole('Owner')) {
832
843
  // Owner can only be edited by owner
833
- hasUserPermission = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
844
+ hasUserPermission = isOwner;
834
845
  } else if (isEitherEditor) {
835
846
  // If the user we are trying to edit is an Author or Contributor, allow it
836
847
  hasUserPermission = userModel.hasRole('Author') || userModel.hasRole('Contributor');
@@ -1058,7 +1069,7 @@ User = ghostBookshelf.Model.extend({
1058
1069
 
1059
1070
  // check if user has the owner role
1060
1071
  const currentRoles = contextUser.toJSON(options).roles;
1061
- if (!_.some(currentRoles, {id: ownerRole.id})) {
1072
+ if (!currentRoles.some(role => role.id === ownerRole.id)) {
1062
1073
  return Promise.reject(new errors.NoPermissionError({
1063
1074
  message: tpl(messages.onlyOwnerCanTransferOwnerRole)
1064
1075
  }));
@@ -1081,7 +1092,7 @@ User = ghostBookshelf.Model.extend({
1081
1092
 
1082
1093
  const {roles: currentRoles, status} = user.toJSON(options);
1083
1094
 
1084
- if (!_.some(currentRoles, {id: adminRole.id})) {
1095
+ if (!currentRoles.some(role => role.id === adminRole.id)) {
1085
1096
  return Promise.reject(new errors.ValidationError({
1086
1097
  message: tpl(messages.onlyAdmCanBeAssignedOwnerRole)
1087
1098
  }));
@@ -1139,4 +1150,4 @@ Users = ghostBookshelf.Collection.extend({
1139
1150
  module.exports = {
1140
1151
  User: ghostBookshelf.model('User', User),
1141
1152
  Users: ghostBookshelf.collection('Users', Users)
1142
- };
1153
+ };
@@ -0,0 +1,13 @@
1
+ const queries = require('../../lib/queries');
2
+
3
+ /**
4
+ * Updates email analytics for a specific member
5
+ *
6
+ * @param {Object} options - The options object
7
+ * @param {string} options.memberId - The ID of the member to update analytics for
8
+ * @returns {Promise<Object>} The result of the aggregation query (1/0)
9
+ */
10
+ module.exports = async function updateMemberEmailAnalytics({memberId}) {
11
+ const result = await queries.aggregateMemberStats(memberId);
12
+ return result;
13
+ };