ghost 5.111.0 → 5.113.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 (167) hide show
  1. package/components/{tryghost-adapter-cache-redis-5.111.0.tgz → tryghost-adapter-cache-redis-5.113.0.tgz} +0 -0
  2. package/components/{tryghost-adapter-manager-5.111.0.tgz → tryghost-adapter-manager-5.113.0.tgz} +0 -0
  3. package/components/tryghost-announcement-bar-settings-5.113.0.tgz +0 -0
  4. package/components/{tryghost-api-framework-5.111.0.tgz → tryghost-api-framework-5.113.0.tgz} +0 -0
  5. package/components/{tryghost-api-version-compatibility-service-5.111.0.tgz → tryghost-api-version-compatibility-service-5.113.0.tgz} +0 -0
  6. package/components/{tryghost-audience-feedback-5.111.0.tgz → tryghost-audience-feedback-5.113.0.tgz} +0 -0
  7. package/components/tryghost-bookshelf-repository-5.113.0.tgz +0 -0
  8. package/components/{tryghost-bootstrap-socket-5.111.0.tgz → tryghost-bootstrap-socket-5.113.0.tgz} +0 -0
  9. package/components/tryghost-captcha-service-5.113.0.tgz +0 -0
  10. package/components/tryghost-constants-5.113.0.tgz +0 -0
  11. package/components/tryghost-custom-fonts-5.113.0.tgz +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.113.0.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.111.0.tgz → tryghost-data-generator-5.113.0.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.113.0.tgz +0 -0
  15. package/components/tryghost-donations-5.113.0.tgz +0 -0
  16. package/components/tryghost-email-addresses-5.113.0.tgz +0 -0
  17. package/components/{tryghost-email-analytics-provider-mailgun-5.111.0.tgz → tryghost-email-analytics-provider-mailgun-5.113.0.tgz} +0 -0
  18. package/components/tryghost-email-analytics-service-5.113.0.tgz +0 -0
  19. package/components/{tryghost-email-content-generator-5.111.0.tgz → tryghost-email-content-generator-5.113.0.tgz} +0 -0
  20. package/components/tryghost-email-events-5.113.0.tgz +0 -0
  21. package/components/tryghost-email-service-5.113.0.tgz +0 -0
  22. package/components/{tryghost-email-suppression-list-5.111.0.tgz → tryghost-email-suppression-list-5.113.0.tgz} +0 -0
  23. package/components/{tryghost-express-dynamic-redirects-5.111.0.tgz → tryghost-express-dynamic-redirects-5.113.0.tgz} +0 -0
  24. package/components/tryghost-extract-api-key-5.113.0.tgz +0 -0
  25. package/components/tryghost-ghost-5.113.0.tgz +0 -0
  26. package/components/{tryghost-html-to-plaintext-5.111.0.tgz → tryghost-html-to-plaintext-5.113.0.tgz} +0 -0
  27. package/components/tryghost-i18n-5.113.0.tgz +0 -0
  28. package/components/{tryghost-identity-token-service-5.111.0.tgz → tryghost-identity-token-service-5.113.0.tgz} +0 -0
  29. package/components/{tryghost-importer-handler-content-files-5.111.0.tgz → tryghost-importer-handler-content-files-5.113.0.tgz} +0 -0
  30. package/components/{tryghost-importer-revue-5.111.0.tgz → tryghost-importer-revue-5.113.0.tgz} +0 -0
  31. package/components/tryghost-in-memory-repository-5.113.0.tgz +0 -0
  32. package/components/tryghost-job-manager-5.113.0.tgz +0 -0
  33. package/components/tryghost-link-redirects-5.113.0.tgz +0 -0
  34. package/components/tryghost-link-replacer-5.113.0.tgz +0 -0
  35. package/components/{tryghost-magic-link-5.111.0.tgz → tryghost-magic-link-5.113.0.tgz} +0 -0
  36. package/components/tryghost-mail-events-5.113.0.tgz +0 -0
  37. package/components/tryghost-mailgun-client-5.113.0.tgz +0 -0
  38. package/components/tryghost-member-attribution-5.113.0.tgz +0 -0
  39. package/components/tryghost-member-events-5.113.0.tgz +0 -0
  40. package/components/{tryghost-members-api-5.111.0.tgz → tryghost-members-api-5.113.0.tgz} +0 -0
  41. package/components/{tryghost-members-csv-5.111.0.tgz → tryghost-members-csv-5.113.0.tgz} +0 -0
  42. package/components/{tryghost-members-importer-5.111.0.tgz → tryghost-members-importer-5.113.0.tgz} +0 -0
  43. package/components/{tryghost-members-offers-5.111.0.tgz → tryghost-members-offers-5.113.0.tgz} +0 -0
  44. package/components/tryghost-members-payments-5.113.0.tgz +0 -0
  45. package/components/{tryghost-members-ssr-5.111.0.tgz → tryghost-members-ssr-5.113.0.tgz} +0 -0
  46. package/components/{tryghost-members-stripe-service-5.111.0.tgz → tryghost-members-stripe-service-5.113.0.tgz} +0 -0
  47. package/components/tryghost-milestones-5.113.0.tgz +0 -0
  48. package/components/{tryghost-minifier-5.111.0.tgz → tryghost-minifier-5.113.0.tgz} +0 -0
  49. package/components/{tryghost-mw-api-version-mismatch-5.111.0.tgz → tryghost-mw-api-version-mismatch-5.113.0.tgz} +0 -0
  50. package/components/tryghost-mw-cache-control-5.113.0.tgz +0 -0
  51. package/components/{tryghost-mw-error-handler-5.111.0.tgz → tryghost-mw-error-handler-5.113.0.tgz} +0 -0
  52. package/components/{tryghost-mw-session-from-token-5.111.0.tgz → tryghost-mw-session-from-token-5.113.0.tgz} +0 -0
  53. package/components/{tryghost-mw-update-user-last-seen-5.111.0.tgz → tryghost-mw-update-user-last-seen-5.113.0.tgz} +0 -0
  54. package/components/{tryghost-mw-version-match-5.111.0.tgz → tryghost-mw-version-match-5.113.0.tgz} +0 -0
  55. package/components/tryghost-mw-vhost-5.113.0.tgz +0 -0
  56. package/components/{tryghost-package-json-5.111.0.tgz → tryghost-package-json-5.113.0.tgz} +0 -0
  57. package/components/tryghost-post-events-5.113.0.tgz +0 -0
  58. package/components/tryghost-post-revisions-5.113.0.tgz +0 -0
  59. package/components/{tryghost-posts-service-5.111.0.tgz → tryghost-posts-service-5.113.0.tgz} +0 -0
  60. package/components/{tryghost-prometheus-metrics-5.111.0.tgz → tryghost-prometheus-metrics-5.113.0.tgz} +0 -0
  61. package/components/tryghost-recommendations-5.113.0.tgz +0 -0
  62. package/components/tryghost-referrers-5.113.0.tgz +0 -0
  63. package/components/{tryghost-security-5.111.0.tgz → tryghost-security-5.113.0.tgz} +0 -0
  64. package/components/tryghost-session-service-5.113.0.tgz +0 -0
  65. package/components/{tryghost-settings-path-manager-5.111.0.tgz → tryghost-settings-path-manager-5.113.0.tgz} +0 -0
  66. package/components/{tryghost-slack-notifications-5.111.0.tgz → tryghost-slack-notifications-5.113.0.tgz} +0 -0
  67. package/components/{tryghost-tiers-5.111.0.tgz → tryghost-tiers-5.113.0.tgz} +0 -0
  68. package/components/tryghost-version-notifications-data-service-5.113.0.tgz +0 -0
  69. package/components/{tryghost-webmentions-5.111.0.tgz → tryghost-webmentions-5.113.0.tgz} +0 -0
  70. package/core/boot.js +0 -3
  71. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +13043 -11763
  72. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-1298238e.mjs → CodeEditorView-ed5e87be.mjs} +2 -2
  73. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  74. package/core/built/admin/assets/admin-x-settings/{index-0f51ccb5.mjs → index-0ee4d13c.mjs} +2 -2
  75. package/core/built/admin/assets/admin-x-settings/{index-2707471f.mjs → index-9c7da716.mjs} +20224 -20178
  76. package/core/built/admin/assets/admin-x-settings/{modals-f5983704.mjs → modals-7708d510.mjs} +2227 -2216
  77. package/core/built/admin/assets/{chunk.524.405c43b2cb20553b51d9.js → chunk.524.4f0aeb6b611079e528f5.js} +7 -7
  78. package/core/built/admin/assets/{chunk.582.eb4b096f29c97c9d6a64.js → chunk.582.485df00698ed27a0668b.js} +8 -8
  79. package/core/built/admin/assets/{ghost-87cffc153ec73d217c1ae9f9207ea5e1.js → ghost-ebf07ae7768b6e9fb9a4b173b6917782.js} +72 -70
  80. package/core/built/admin/assets/img/ap-nodes-01ee317529e6353a1c34a062c388f1e7.png +0 -0
  81. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12944 -12924
  82. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +136 -134
  83. package/core/built/admin/assets/posts/posts.js +2 -2
  84. package/core/built/admin/assets/{vendor-fca15534b8426c0567400113c63a3e21.js → vendor-68a4aa424a179a90f5bbc2b750def576.js} +28 -26
  85. package/core/built/admin/index.html +4 -4
  86. package/core/frontend/helpers/get.js +2 -3
  87. package/core/frontend/services/routing/registry.js +6 -6
  88. package/core/frontend/src/admin-auth/message-handler.js +1 -1
  89. package/core/frontend/src/cards/css/cta.css +38 -36
  90. package/core/server/adapters/cache/AdapterCacheMemoryTTL.js +54 -0
  91. package/core/server/adapters/cache/memory-ttl.js +1 -1
  92. package/core/server/api/endpoints/utils/serializers/input/settings.js +2 -1
  93. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +2 -1
  94. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +2 -1
  95. package/core/server/data/migrations/versions/5.112/2025-03-10-10-01-01-add-require-mfa-setting.js +8 -0
  96. package/core/server/data/migrations/versions/5.113/2025-03-07-12-24-00-add-super-editor.js +31 -0
  97. package/core/server/data/migrations/versions/5.113/2025-03-07-12-25-00-add-member-perms-to-super-editor.js +291 -0
  98. package/core/server/data/schema/default-settings/default-settings.json +6 -0
  99. package/core/server/data/schema/fixtures/fixtures.json +27 -0
  100. package/core/server/models/invite.js +6 -7
  101. package/core/server/models/post.js +3 -9
  102. package/core/server/models/relations/authors.js +2 -4
  103. package/core/server/models/role-utils.js +38 -0
  104. package/core/server/models/role.js +7 -5
  105. package/core/server/models/user.js +41 -28
  106. package/core/server/services/activitypub/ActivityPubService.js +116 -0
  107. package/core/server/services/activitypub/ActivityPubService.ts +139 -0
  108. package/core/server/services/activitypub/ActivityPubServiceWrapper.js +1 -1
  109. package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +13 -0
  110. package/core/server/services/email-analytics/lib/queries.js +3 -3
  111. package/core/server/services/media-inliner/ExternalMediaInliner.js +346 -0
  112. package/core/server/services/media-inliner/service.js +1 -1
  113. package/core/server/services/permissions/can-this.js +3 -2
  114. package/core/server/services/stats/MembersStatsService.js +167 -0
  115. package/core/server/services/stats/MrrStatsService.js +161 -0
  116. package/core/server/services/stats/ReferrersStatsService.js +164 -0
  117. package/core/server/services/stats/StatsService.js +63 -0
  118. package/core/server/services/stats/SubscriptionStatsService.js +180 -0
  119. package/core/server/services/stats/service.js +1 -1
  120. package/core/server/services/url/Resources.js +20 -30
  121. package/core/server/services/url/UrlService.js +2 -12
  122. package/core/server/services/url/Urls.js +17 -33
  123. package/core/shared/config/defaults.json +1 -1
  124. package/core/shared/labs.js +2 -1
  125. package/core/shared/settings-cache/CacheManager.js +4 -4
  126. package/package.json +139 -142
  127. package/tsconfig.tsbuildinfo +1 -1
  128. package/yarn.lock +40 -82
  129. package/components/tryghost-activitypub-5.111.0.tgz +0 -0
  130. package/components/tryghost-adapter-cache-memory-ttl-5.111.0.tgz +0 -0
  131. package/components/tryghost-announcement-bar-settings-5.111.0.tgz +0 -0
  132. package/components/tryghost-bookshelf-repository-5.111.0.tgz +0 -0
  133. package/components/tryghost-captcha-service-5.111.0.tgz +0 -0
  134. package/components/tryghost-constants-5.111.0.tgz +0 -0
  135. package/components/tryghost-custom-fonts-5.111.0.tgz +0 -0
  136. package/components/tryghost-custom-theme-settings-service-5.111.0.tgz +0 -0
  137. package/components/tryghost-domain-events-5.111.0.tgz +0 -0
  138. package/components/tryghost-donations-5.111.0.tgz +0 -0
  139. package/components/tryghost-email-addresses-5.111.0.tgz +0 -0
  140. package/components/tryghost-email-analytics-service-5.111.0.tgz +0 -0
  141. package/components/tryghost-email-events-5.111.0.tgz +0 -0
  142. package/components/tryghost-email-service-5.111.0.tgz +0 -0
  143. package/components/tryghost-external-media-inliner-5.111.0.tgz +0 -0
  144. package/components/tryghost-extract-api-key-5.111.0.tgz +0 -0
  145. package/components/tryghost-ghost-5.111.0.tgz +0 -0
  146. package/components/tryghost-i18n-5.111.0.tgz +0 -0
  147. package/components/tryghost-in-memory-repository-5.111.0.tgz +0 -0
  148. package/components/tryghost-job-manager-5.111.0.tgz +0 -0
  149. package/components/tryghost-link-redirects-5.111.0.tgz +0 -0
  150. package/components/tryghost-link-replacer-5.111.0.tgz +0 -0
  151. package/components/tryghost-mail-events-5.111.0.tgz +0 -0
  152. package/components/tryghost-mailgun-client-5.111.0.tgz +0 -0
  153. package/components/tryghost-member-attribution-5.111.0.tgz +0 -0
  154. package/components/tryghost-member-events-5.111.0.tgz +0 -0
  155. package/components/tryghost-members-payments-5.111.0.tgz +0 -0
  156. package/components/tryghost-milestones-5.111.0.tgz +0 -0
  157. package/components/tryghost-mw-cache-control-5.111.0.tgz +0 -0
  158. package/components/tryghost-mw-vhost-5.111.0.tgz +0 -0
  159. package/components/tryghost-post-events-5.111.0.tgz +0 -0
  160. package/components/tryghost-post-revisions-5.111.0.tgz +0 -0
  161. package/components/tryghost-recommendations-5.111.0.tgz +0 -0
  162. package/components/tryghost-referrers-5.111.0.tgz +0 -0
  163. package/components/tryghost-session-service-5.111.0.tgz +0 -0
  164. package/components/tryghost-stats-service-5.111.0.tgz +0 -0
  165. package/components/tryghost-version-notifications-data-service-5.111.0.tgz +0 -0
  166. package/core/demo.js +0 -6
  167. package/core/demo.ts +0 -3
@@ -21,6 +21,7 @@ const {BadRequestError} = require('@tryghost/errors');
21
21
  const {PostRevisions} = require('@tryghost/post-revisions');
22
22
  const {mobiledocToLexical} = require('@tryghost/kg-converters');
23
23
  const labs = require('../../shared/labs');
24
+ const {setIsRoles} = require('./role-utils');
24
25
 
25
26
  const messages = {
26
27
  isAlreadyPublished: 'Your post is already published, please reload your page.',
@@ -1478,10 +1479,7 @@ Post = ghostBookshelf.Model.extend({
1478
1479
 
1479
1480
  // NOTE: the `authors` extension is the parent of the post model. It also has a permissible function.
1480
1481
  permissible: async function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
1481
- let isContributor;
1482
- let isOwner;
1483
- let isAdmin;
1484
- let isEditor;
1482
+ let {isContributor, isOwner, isAdmin, isEitherEditor} = setIsRoles(loadedPermissions);
1485
1483
  let isIntegration;
1486
1484
  let isEdit;
1487
1485
  let isAdd;
@@ -1499,10 +1497,6 @@ Post = ghostBookshelf.Model.extend({
1499
1497
  return postModel.get('status') === 'draft';
1500
1498
  }
1501
1499
 
1502
- isContributor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Contributor'});
1503
- isOwner = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
1504
- isAdmin = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Administrator'});
1505
- isEditor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'});
1506
1500
  isIntegration = loadedPermissions.apiKey && _.some(loadedPermissions.apiKey.roles, {name: 'Admin Integration'});
1507
1501
 
1508
1502
  isEdit = (action === 'edit');
@@ -1525,7 +1519,7 @@ Post = ghostBookshelf.Model.extend({
1525
1519
  } else if (isContributor && isDestroy) {
1526
1520
  // If destroying, only allow contributor to destroy their own draft posts
1527
1521
  hasUserPermission = isDraft();
1528
- } else if (!(isOwner || isAdmin || isEditor || isIntegration)) {
1522
+ } else if (!(isOwner || isAdmin || isEitherEditor || isIntegration)) {
1529
1523
  hasUserPermission = !isChanging('visibility');
1530
1524
  }
1531
1525
 
@@ -2,6 +2,7 @@ const _ = require('lodash');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const {sequence} = require('@tryghost/promise');
5
+ const {setIsRoles} = require('../role-utils');
5
6
 
6
7
  const messages = {
7
8
  noUserFound: 'No user found',
@@ -305,8 +306,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
305
306
  const self = this;
306
307
  const postModel = postModelOrId;
307
308
  let origArgs;
308
- let isContributor;
309
- let isAuthor;
309
+ const {isContributor, isAuthor} = setIsRoles(loadedPermissions);
310
310
  let isEdit;
311
311
  let isAdd;
312
312
  let isDestroy;
@@ -332,8 +332,6 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
332
332
  });
333
333
  }
334
334
 
335
- isContributor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Contributor'});
336
- isAuthor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Author'});
337
335
  isEdit = (action === 'edit');
338
336
  isAdd = (action === 'add');
339
337
  isDestroy = (action === 'destroy');
@@ -0,0 +1,38 @@
1
+ // check if the user has an assigned role
2
+ // so that we can stop writing this everywhere:
3
+ //_.some(loadedPermissions.user.roles, {name: 'Administrator'})
4
+
5
+ function checkUserPermissionsForRole(loadedPermissions, roleName) {
6
+ if (!loadedPermissions?.user?.roles) {
7
+ return false;
8
+ }
9
+
10
+ return loadedPermissions.user.roles.some(role => role.name === roleName);
11
+ }
12
+
13
+ function setIsRoles(loadedPermissions) {
14
+ // utility function to parse the permissions object and set up all the "is" variables.
15
+ let resultsObject = {
16
+ isOwner: false,
17
+ isAdmin: false,
18
+ isEditor: false,
19
+ isAuthor: false,
20
+ isContributor: false,
21
+ isSuperEditor: false,
22
+ isEitherEditor: false
23
+ };
24
+ if (!loadedPermissions?.user?.roles) {
25
+ return resultsObject;
26
+ }
27
+ resultsObject.isOwner = checkUserPermissionsForRole(loadedPermissions, 'Owner');
28
+ resultsObject.isAdmin = checkUserPermissionsForRole(loadedPermissions, 'Administrator');
29
+ resultsObject.isEditor = checkUserPermissionsForRole(loadedPermissions, 'Editor');
30
+ resultsObject.isAuthor = checkUserPermissionsForRole(loadedPermissions, 'Author');
31
+ resultsObject.isContributor = checkUserPermissionsForRole(loadedPermissions, 'Contributor');
32
+ resultsObject.isSuperEditor = checkUserPermissionsForRole(loadedPermissions, 'Super Editor');
33
+ resultsObject.isEitherEditor = resultsObject.isEditor || resultsObject.isSuperEditor;
34
+ return resultsObject;
35
+ }
36
+
37
+ exports.setIsRoles = setIsRoles;
38
+ exports.checkUserPermissionsForRole = checkUserPermissionsForRole;
@@ -2,6 +2,7 @@ const _ = require('lodash');
2
2
  const ghostBookshelf = require('./base');
3
3
  const tpl = require('@tryghost/tpl');
4
4
  const errors = require('@tryghost/errors');
5
+ const {setIsRoles} = require('./role-utils');
5
6
 
6
7
  const messages = {
7
8
  roleNotFound: 'Role not found',
@@ -78,12 +79,13 @@ Role = ghostBookshelf.Model.extend({
78
79
  const roleModel = roleModelOrId;
79
80
 
80
81
  if (action === 'assign' && loadedPermissions.user) {
82
+ const {isOwner, isAdmin, isEitherEditor} = setIsRoles(loadedPermissions);
81
83
  let checkAgainst;
82
- if (_.some(loadedPermissions.user.roles, {name: 'Owner'})) {
83
- checkAgainst = ['Owner', 'Administrator', 'Editor', 'Author', 'Contributor'];
84
- } else if (_.some(loadedPermissions.user.roles, {name: 'Administrator'})) {
85
- checkAgainst = ['Administrator', 'Editor', 'Author', 'Contributor'];
86
- } else if (_.some(loadedPermissions.user.roles, {name: 'Editor'})) {
84
+ if (isOwner) {
85
+ checkAgainst = ['Owner', 'Administrator', 'Super Editor', 'Editor', 'Author', 'Contributor'];
86
+ } else if (isAdmin) {
87
+ checkAgainst = ['Administrator', 'Super Editor', 'Editor', 'Author', 'Contributor'];
88
+ } else if (isEitherEditor) {
87
89
  checkAgainst = ['Author', 'Contributor'];
88
90
  }
89
91
 
@@ -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,8 +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');
13
+ const {setIsRoles} = require('./role-utils');
14
14
  const activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'];
15
- const ASSIGNABLE_ROLES = ['Administrator', 'Editor', 'Author', 'Contributor'];
15
+ const ASSIGNABLE_ROLES = ['Administrator', 'Super Editor', 'Editor', 'Author', 'Contributor'];
16
16
 
17
17
  const messages = {
18
18
  valueCannotBeBlank: 'Value in [{tableName}.{columnKey}] cannot be blank.',
@@ -76,7 +76,7 @@ User = ghostBookshelf.Model.extend({
76
76
  },
77
77
 
78
78
  format(options) {
79
- if (!_.isEmpty(options.website) &&
79
+ if (options.website &&
80
80
  !validator.isURL(options.website, {
81
81
  require_protocol: true,
82
82
  protocols: ['http', 'https']
@@ -129,7 +129,7 @@ User = ghostBookshelf.Model.extend({
129
129
  onDestroyed: function onDestroyed(model, options) {
130
130
  ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);
131
131
 
132
- if (_.includes(activeStates, model.previous('status'))) {
132
+ if (activeStates.includes(model.previous('status'))) {
133
133
  model.emitChange('deactivated', options);
134
134
  }
135
135
 
@@ -142,7 +142,7 @@ User = ghostBookshelf.Model.extend({
142
142
  model.emitChange('added', options);
143
143
 
144
144
  // active is the default state, so if status isn't provided, this will be an active user
145
- if (!model.get('status') || _.includes(activeStates, model.get('status'))) {
145
+ if (!model.get('status') || activeStates.includes(model.get('status'))) {
146
146
  model.emitChange('activated', options);
147
147
  }
148
148
  },
@@ -151,7 +151,7 @@ User = ghostBookshelf.Model.extend({
151
151
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
152
152
 
153
153
  model.statusChanging = model.get('status') !== model.previous('status');
154
- model.isActive = _.includes(activeStates, model.get('status'));
154
+ model.isActive = activeStates.includes(model.get('status'));
155
155
 
156
156
  if (model.statusChanging) {
157
157
  model.emitChange(model.isActive ? 'activated' : 'deactivated', options);
@@ -455,18 +455,16 @@ User = ghostBookshelf.Model.extend({
455
455
  const options = this.filterOptions(unfilteredOptions, 'findOne');
456
456
  let query;
457
457
  let status;
458
- let data = _.cloneDeep(dataToClone);
458
+ let data = JSON.parse(JSON.stringify(dataToClone));
459
459
  const lookupRole = data.role;
460
460
 
461
461
  // Ensure only valid fields/columns are added to query
462
462
  if (options.columns) {
463
- options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
463
+ options.columns = options.columns.filter(col => this.prototype.permittedAttributes().includes(col));
464
464
  }
465
465
 
466
466
  delete data.role;
467
- data = _.defaults(data || {}, {
468
- status: 'all'
469
- });
467
+ data = Object.assign({}, {status: 'all'}, data || {});
470
468
 
471
469
  status = data.status;
472
470
  delete data.status;
@@ -475,7 +473,7 @@ User = ghostBookshelf.Model.extend({
475
473
 
476
474
  // Support finding by role
477
475
  if (lookupRole) {
478
- options.withRelated = _.union(options.withRelated, ['roles']);
476
+ options.withRelated = [...new Set([...(options.withRelated || []), 'roles'])];
479
477
  query = this.forge(data);
480
478
 
481
479
  query.query('join', 'roles_users', 'users.id', '=', 'roles_users.user_id');
@@ -519,7 +517,7 @@ User = ghostBookshelf.Model.extend({
519
517
  } else if (type === 'recommendation-received') {
520
518
  filter += '+recommendation_notifications:true';
521
519
  }
522
- const updatedOptions = _.merge({}, options, {filter, withRelated: ['roles']});
520
+ const updatedOptions = Object.assign({}, options, {filter, withRelated: ['roles']});
523
521
  return this.findAll(updatedOptions).then((users) => {
524
522
  return users.toJSON().filter((user) => {
525
523
  return user?.roles?.some((role) => {
@@ -640,7 +638,7 @@ User = ghostBookshelf.Model.extend({
640
638
  add: function add(dataToClone, unfilteredOptions) {
641
639
  const options = this.filterOptions(unfilteredOptions, 'add');
642
640
  const self = this;
643
- const data = _.cloneDeep(dataToClone);
641
+ const data = JSON.parse(JSON.stringify(dataToClone));
644
642
  let userData = this.filterData(data);
645
643
  let roles;
646
644
 
@@ -652,7 +650,7 @@ User = ghostBookshelf.Model.extend({
652
650
  }
653
651
 
654
652
  function getAuthorRole() {
655
- return ghostBookshelf.model('Role').findOne({name: 'Author'}, _.pick(options, 'transacting'))
653
+ return ghostBookshelf.model('Role').findOne({name: 'Author'}, {transacting: options.transacting})
656
654
  .then(function then(authorRole) {
657
655
  return [authorRole.get('id')];
658
656
  });
@@ -683,7 +681,7 @@ User = ghostBookshelf.Model.extend({
683
681
  roles = _roles;
684
682
 
685
683
  // CASE: it is possible to add roles by name, by id or by object
686
- if (_.isString(roles[0]) && !ObjectId.isValid(roles[0])) {
684
+ if (typeof roles[0] === 'string' && !ObjectId.isValid(roles[0])) {
687
685
  const rolePromises = roles.map((roleName) => {
688
686
  return ghostBookshelf.model('Role').findOne({
689
687
  name: roleName
@@ -780,19 +778,37 @@ User = ghostBookshelf.Model.extend({
780
778
  });
781
779
  },
782
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
+ */
783
798
  permissible: async function permissible(userModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
784
799
  const self = this;
785
800
  const userModel = userModelOrId;
786
801
  let origArgs;
802
+ const {isOwner, isEitherEditor} = setIsRoles(loadedPermissions);
787
803
 
788
804
  // If we passed in a model without its related roles, we need to fetch it again
789
- if (_.isObject(userModelOrId) && !_.isObject(userModelOrId.related('roles'))) {
805
+ if (typeof userModelOrId === 'object' && !(typeof userModelOrId.related('roles') === 'object')) {
790
806
  userModelOrId = userModelOrId.id;
791
807
  }
792
808
  // If we passed in an id instead of a model get the model first
793
- if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) {
809
+ if (typeof userModelOrId === 'number' || typeof userModelOrId === 'string') {
794
810
  // Grab the original args without the first one
795
- origArgs = _.toArray(arguments).slice(1);
811
+ origArgs = Array.from(arguments).slice(1);
796
812
 
797
813
  // Get the actual user model
798
814
  return this.findOne({
@@ -820,16 +836,13 @@ User = ghostBookshelf.Model.extend({
820
836
  }
821
837
 
822
838
  if (action === 'edit') {
823
- // Users with the role 'Editor', 'Author', and 'Contributor' have complex permissions when the action === 'edit'
824
- // We now have all the info we need to construct the permissions
825
-
826
839
  if (context.user === userModel.get('id')) {
827
840
  // If this is the same user that requests the operation allow it.
828
841
  hasUserPermission = true;
829
842
  } else if (loadedPermissions.user && userModel.hasRole('Owner')) {
830
843
  // Owner can only be edited by owner
831
- hasUserPermission = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
832
- } else if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
844
+ hasUserPermission = isOwner;
845
+ } else if (isEitherEditor) {
833
846
  // If the user we are trying to edit is an Author or Contributor, allow it
834
847
  hasUserPermission = userModel.hasRole('Author') || userModel.hasRole('Contributor');
835
848
  }
@@ -844,7 +857,7 @@ User = ghostBookshelf.Model.extend({
844
857
  }
845
858
 
846
859
  // Users with the role 'Editor' have complex permissions when the action === 'destroy'
847
- if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
860
+ if (isEitherEditor) {
848
861
  // Alternatively, if the user we are trying to edit is an Author, allow it
849
862
  hasUserPermission = context.user === userModel.get('id') || userModel.hasRole('Author') || userModel.hasRole('Contributor');
850
863
  }
@@ -1056,7 +1069,7 @@ User = ghostBookshelf.Model.extend({
1056
1069
 
1057
1070
  // check if user has the owner role
1058
1071
  const currentRoles = contextUser.toJSON(options).roles;
1059
- if (!_.some(currentRoles, {id: ownerRole.id})) {
1072
+ if (!currentRoles.some(role => role.id === ownerRole.id)) {
1060
1073
  return Promise.reject(new errors.NoPermissionError({
1061
1074
  message: tpl(messages.onlyOwnerCanTransferOwnerRole)
1062
1075
  }));
@@ -1079,7 +1092,7 @@ User = ghostBookshelf.Model.extend({
1079
1092
 
1080
1093
  const {roles: currentRoles, status} = user.toJSON(options);
1081
1094
 
1082
- if (!_.some(currentRoles, {id: adminRole.id})) {
1095
+ if (!currentRoles.some(role => role.id === adminRole.id)) {
1083
1096
  return Promise.reject(new errors.ValidationError({
1084
1097
  message: tpl(messages.onlyAdmCanBeAssignedOwnerRole)
1085
1098
  }));
@@ -1137,4 +1150,4 @@ Users = ghostBookshelf.Collection.extend({
1137
1150
  module.exports = {
1138
1151
  User: ghostBookshelf.model('User', User),
1139
1152
  Users: ghostBookshelf.collection('Users', Users)
1140
- };
1153
+ };
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ActivityPubService = void 0;
7
+ const bson_objectid_1 = __importDefault(require("bson-objectid"));
8
+ const node_fetch_1 = __importDefault(require("node-fetch"));
9
+ class ActivityPubService {
10
+ knex;
11
+ siteUrl;
12
+ logging;
13
+ identityTokenService;
14
+ constructor(knex, siteUrl, logging, identityTokenService) {
15
+ this.knex = knex;
16
+ this.siteUrl = siteUrl;
17
+ this.logging = logging;
18
+ this.identityTokenService = identityTokenService;
19
+ }
20
+ getExpectedWebhooks(secret) {
21
+ return [{
22
+ event: 'post.published',
23
+ target_url: new URL('.ghost/activitypub/webhooks/post/published', this.siteUrl),
24
+ api_version: 'v5.100.0',
25
+ secret
26
+ }, {
27
+ event: 'site.changed',
28
+ target_url: new URL('.ghost/activitypub/webhooks/site/changed', this.siteUrl),
29
+ api_version: 'v5.100.0',
30
+ secret
31
+ }];
32
+ }
33
+ async checkWebhookState(expectedWebhooks, integration) {
34
+ this.logging.info(`Checking ActivityPub Webhook state`);
35
+ const webhooks = await this.knex
36
+ .select('*')
37
+ .from('webhooks')
38
+ .where('integration_id', '=', integration.id);
39
+ if (webhooks.length !== expectedWebhooks.length) {
40
+ this.logging.warn(`Expected ${expectedWebhooks.length} webhooks for ActivityPub`);
41
+ return false;
42
+ }
43
+ for (const expectedWebhook of expectedWebhooks) {
44
+ const foundWebhook = webhooks.find((webhook) => {
45
+ return webhook.event === expectedWebhook.event && webhook.target_url === expectedWebhook.target_url.href && webhook.secret === expectedWebhook.secret;
46
+ });
47
+ if (!foundWebhook) {
48
+ this.logging.error(`Could not find webhook for ${expectedWebhook.event} ${expectedWebhook.target_url}`);
49
+ return false;
50
+ }
51
+ }
52
+ return true;
53
+ }
54
+ async getWebhookSecret() {
55
+ try {
56
+ const ownerUser = await this.knex.select('*').from('users').where('id', '=', '1').first();
57
+ const token = await this.identityTokenService.getTokenForUser(ownerUser.email, 'Owner');
58
+ const res = await (0, node_fetch_1.default)(new URL('.ghost/activitypub/site', this.siteUrl), {
59
+ headers: {
60
+ Authorization: `Bearer ${token}`
61
+ }
62
+ });
63
+ const body = await res.json();
64
+ return body.webhook_secret;
65
+ }
66
+ catch (err) {
67
+ this.logging.error(`Could not get webhook secret for ActivityPub ${err}`);
68
+ return null;
69
+ }
70
+ }
71
+ async initialiseWebhooks() {
72
+ const integration = await this.knex
73
+ .select('*')
74
+ .from('integrations')
75
+ .where('slug', '=', 'ghost-activitypub')
76
+ .andWhere('type', '=', 'internal')
77
+ .first();
78
+ if (!integration) {
79
+ this.logging.error('No ActivityPub integration found - cannot initialise');
80
+ return;
81
+ }
82
+ const secret = await this.getWebhookSecret();
83
+ if (!secret) {
84
+ this.logging.error('No webhook secret found - cannot initialise');
85
+ return;
86
+ }
87
+ const expectedWebhooks = this.getExpectedWebhooks(secret);
88
+ const isInCorrectState = await this.checkWebhookState(expectedWebhooks, integration);
89
+ if (isInCorrectState) {
90
+ this.logging.info(`ActivityPub webhooks in correct state`);
91
+ return;
92
+ }
93
+ this.logging.info(`ActivityPub webhooks in incorrect state, deleting all of them and starting fresh`);
94
+ await this.knex
95
+ .del()
96
+ .from('webhooks')
97
+ .where('integration_id', '=', integration.id);
98
+ const webhooksToInsert = expectedWebhooks.map((expectedWebhook) => {
99
+ return {
100
+ id: (new bson_objectid_1.default).toHexString(),
101
+ event: expectedWebhook.event,
102
+ target_url: expectedWebhook.target_url.href,
103
+ api_version: expectedWebhook.api_version,
104
+ name: `ActivityPub ${expectedWebhook.event} Webhook`,
105
+ secret: secret,
106
+ integration_id: integration.id,
107
+ created_at: this.knex.raw('current_timestamp'),
108
+ created_by: '1'
109
+ };
110
+ });
111
+ await this.knex
112
+ .insert(webhooksToInsert)
113
+ .into('webhooks');
114
+ }
115
+ }
116
+ exports.ActivityPubService = ActivityPubService;
@@ -0,0 +1,139 @@
1
+ import ObjectID from 'bson-objectid';
2
+ import {Knex} from 'knex';
3
+ import {IdentityTokenService} from '@tryghost/identity-token-service';
4
+ import fetch from 'node-fetch';
5
+
6
+ type ExpectedWebhook = {
7
+ event: string;
8
+ target_url: URL;
9
+ api_version: string;
10
+ secret: string;
11
+ };
12
+
13
+ interface Logger {
14
+ info(message: string): void
15
+ warn(message: string): void
16
+ error(message: string): void
17
+ }
18
+
19
+ export class ActivityPubService {
20
+ constructor(
21
+ private knex: Knex,
22
+ private siteUrl: URL,
23
+ private logging: Logger,
24
+ private identityTokenService: IdentityTokenService
25
+ ) {}
26
+
27
+ getExpectedWebhooks(secret: string): ExpectedWebhook[] {
28
+ return [{
29
+ event: 'post.published',
30
+ target_url: new URL('.ghost/activitypub/webhooks/post/published', this.siteUrl),
31
+ api_version: 'v5.100.0',
32
+ secret
33
+ }, {
34
+ event: 'site.changed',
35
+ target_url: new URL('.ghost/activitypub/webhooks/site/changed', this.siteUrl),
36
+ api_version: 'v5.100.0',
37
+ secret
38
+ }];
39
+ }
40
+
41
+ async checkWebhookState(expectedWebhooks: ExpectedWebhook[], integration: {id: string}) {
42
+ this.logging.info(`Checking ActivityPub Webhook state`);
43
+
44
+ const webhooks = await this.knex
45
+ .select('*')
46
+ .from('webhooks')
47
+ .where('integration_id', '=', integration.id);
48
+
49
+ if (webhooks.length !== expectedWebhooks.length) {
50
+ this.logging.warn(`Expected ${expectedWebhooks.length} webhooks for ActivityPub`);
51
+ return false;
52
+ }
53
+
54
+ for (const expectedWebhook of expectedWebhooks) {
55
+ const foundWebhook = webhooks.find((webhook) => {
56
+ return webhook.event === expectedWebhook.event && webhook.target_url === expectedWebhook.target_url.href && webhook.secret === expectedWebhook.secret;
57
+ });
58
+ if (!foundWebhook) {
59
+ this.logging.error(`Could not find webhook for ${expectedWebhook.event} ${expectedWebhook.target_url}`);
60
+ return false;
61
+ }
62
+ }
63
+
64
+ return true;
65
+ }
66
+
67
+ async getWebhookSecret(): Promise<string | null> {
68
+ try {
69
+ const ownerUser = await this.knex.select('*').from('users').where('id', '=', '1').first();
70
+ const token = await this.identityTokenService.getTokenForUser(ownerUser.email, 'Owner');
71
+
72
+ const res = await fetch(new URL('.ghost/activitypub/site', this.siteUrl), {
73
+ headers: {
74
+ Authorization: `Bearer ${token}`
75
+ }
76
+ });
77
+
78
+ const body = await res.json();
79
+
80
+ return body.webhook_secret;
81
+ } catch (err: unknown) {
82
+ this.logging.error(`Could not get webhook secret for ActivityPub ${err}`);
83
+ return null;
84
+ }
85
+ }
86
+
87
+ async initialiseWebhooks() {
88
+ const integration = await this.knex
89
+ .select('*')
90
+ .from('integrations')
91
+ .where('slug', '=', 'ghost-activitypub')
92
+ .andWhere('type', '=', 'internal')
93
+ .first();
94
+
95
+ if (!integration) {
96
+ this.logging.error('No ActivityPub integration found - cannot initialise');
97
+ return;
98
+ }
99
+
100
+ const secret = await this.getWebhookSecret();
101
+
102
+ if (!secret) {
103
+ this.logging.error('No webhook secret found - cannot initialise');
104
+ return;
105
+ }
106
+
107
+ const expectedWebhooks = this.getExpectedWebhooks(secret);
108
+ const isInCorrectState = await this.checkWebhookState(expectedWebhooks, integration);
109
+
110
+ if (isInCorrectState) {
111
+ this.logging.info(`ActivityPub webhooks in correct state`);
112
+ return;
113
+ }
114
+
115
+ this.logging.info(`ActivityPub webhooks in incorrect state, deleting all of them and starting fresh`);
116
+ await this.knex
117
+ .del()
118
+ .from('webhooks')
119
+ .where('integration_id', '=', integration.id);
120
+
121
+ const webhooksToInsert = expectedWebhooks.map((expectedWebhook) => {
122
+ return {
123
+ id: (new ObjectID).toHexString(),
124
+ event: expectedWebhook.event,
125
+ target_url: expectedWebhook.target_url.href,
126
+ api_version: expectedWebhook.api_version,
127
+ name: `ActivityPub ${expectedWebhook.event} Webhook`,
128
+ secret: secret,
129
+ integration_id: integration.id,
130
+ created_at: this.knex.raw('current_timestamp'),
131
+ created_by: '1'
132
+ };
133
+ });
134
+
135
+ await this.knex
136
+ .insert(webhooksToInsert)
137
+ .into('webhooks');
138
+ }
139
+ }
@@ -1,4 +1,4 @@
1
- const {ActivityPubService} = require('@tryghost/activitypub');
1
+ const {ActivityPubService} = require('./ActivityPubService');
2
2
 
3
3
  module.exports = class ActivityPubServiceWrapper {
4
4
  /** @type ActivityPubService */
@@ -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
+ };
@@ -99,7 +99,7 @@ module.exports = {
99
99
  /**
100
100
  * Sets the timestamp of the last seen event for the specified email analytics events.
101
101
  * @param {EmailAnalyticsJobName} jobName - The name of the job to update.
102
- * @param {'completed'|'started'} field - The field to update.
102
+ * @param {'finished'|'started'} field - The field to update.
103
103
  * @param {Date} date - The timestamp of the last seen event.
104
104
  * @returns {Promise<void>}
105
105
  * @description
@@ -110,8 +110,8 @@ module.exports = {
110
110
  // Convert string dates to Date objects for SQLite compatibility
111
111
  try {
112
112
  debug(`Setting ${field} timestamp for job ${jobName} to ${date}`);
113
- const updateField = field === 'completed' ? 'finished_at' : 'started_at';
114
- const status = field === 'completed' ? 'finished' : 'started';
113
+ const updateField = field === 'finished' ? 'finished_at' : 'started_at';
114
+ const status = field === 'finished' ? 'finished' : 'started';
115
115
  const result = await db.knex('jobs').update({[updateField]: date, updated_at: new Date(), status: status}).where('name', jobName);
116
116
  if (result === 0) {
117
117
  await db.knex('jobs').insert({