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.
- package/components/{tryghost-adapter-cache-redis-5.111.0.tgz → tryghost-adapter-cache-redis-5.113.0.tgz} +0 -0
- package/components/{tryghost-adapter-manager-5.111.0.tgz → tryghost-adapter-manager-5.113.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.113.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.111.0.tgz → tryghost-api-framework-5.113.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.111.0.tgz → tryghost-api-version-compatibility-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-audience-feedback-5.111.0.tgz → tryghost-audience-feedback-5.113.0.tgz} +0 -0
- package/components/tryghost-bookshelf-repository-5.113.0.tgz +0 -0
- package/components/{tryghost-bootstrap-socket-5.111.0.tgz → tryghost-bootstrap-socket-5.113.0.tgz} +0 -0
- package/components/tryghost-captcha-service-5.113.0.tgz +0 -0
- package/components/tryghost-constants-5.113.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.113.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.113.0.tgz +0 -0
- package/components/{tryghost-data-generator-5.111.0.tgz → tryghost-data-generator-5.113.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.113.0.tgz +0 -0
- package/components/tryghost-donations-5.113.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.113.0.tgz +0 -0
- package/components/{tryghost-email-analytics-provider-mailgun-5.111.0.tgz → tryghost-email-analytics-provider-mailgun-5.113.0.tgz} +0 -0
- package/components/tryghost-email-analytics-service-5.113.0.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.111.0.tgz → tryghost-email-content-generator-5.113.0.tgz} +0 -0
- package/components/tryghost-email-events-5.113.0.tgz +0 -0
- package/components/tryghost-email-service-5.113.0.tgz +0 -0
- package/components/{tryghost-email-suppression-list-5.111.0.tgz → tryghost-email-suppression-list-5.113.0.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.111.0.tgz → tryghost-express-dynamic-redirects-5.113.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.113.0.tgz +0 -0
- package/components/tryghost-ghost-5.113.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.111.0.tgz → tryghost-html-to-plaintext-5.113.0.tgz} +0 -0
- package/components/tryghost-i18n-5.113.0.tgz +0 -0
- package/components/{tryghost-identity-token-service-5.111.0.tgz → tryghost-identity-token-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-importer-handler-content-files-5.111.0.tgz → tryghost-importer-handler-content-files-5.113.0.tgz} +0 -0
- package/components/{tryghost-importer-revue-5.111.0.tgz → tryghost-importer-revue-5.113.0.tgz} +0 -0
- package/components/tryghost-in-memory-repository-5.113.0.tgz +0 -0
- package/components/tryghost-job-manager-5.113.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.113.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.113.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.111.0.tgz → tryghost-magic-link-5.113.0.tgz} +0 -0
- package/components/tryghost-mail-events-5.113.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.113.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.113.0.tgz +0 -0
- package/components/tryghost-member-events-5.113.0.tgz +0 -0
- package/components/{tryghost-members-api-5.111.0.tgz → tryghost-members-api-5.113.0.tgz} +0 -0
- package/components/{tryghost-members-csv-5.111.0.tgz → tryghost-members-csv-5.113.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.111.0.tgz → tryghost-members-importer-5.113.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.111.0.tgz → tryghost-members-offers-5.113.0.tgz} +0 -0
- package/components/tryghost-members-payments-5.113.0.tgz +0 -0
- package/components/{tryghost-members-ssr-5.111.0.tgz → tryghost-members-ssr-5.113.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.111.0.tgz → tryghost-members-stripe-service-5.113.0.tgz} +0 -0
- package/components/tryghost-milestones-5.113.0.tgz +0 -0
- package/components/{tryghost-minifier-5.111.0.tgz → tryghost-minifier-5.113.0.tgz} +0 -0
- package/components/{tryghost-mw-api-version-mismatch-5.111.0.tgz → tryghost-mw-api-version-mismatch-5.113.0.tgz} +0 -0
- package/components/tryghost-mw-cache-control-5.113.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.111.0.tgz → tryghost-mw-error-handler-5.113.0.tgz} +0 -0
- package/components/{tryghost-mw-session-from-token-5.111.0.tgz → tryghost-mw-session-from-token-5.113.0.tgz} +0 -0
- package/components/{tryghost-mw-update-user-last-seen-5.111.0.tgz → tryghost-mw-update-user-last-seen-5.113.0.tgz} +0 -0
- package/components/{tryghost-mw-version-match-5.111.0.tgz → tryghost-mw-version-match-5.113.0.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.113.0.tgz +0 -0
- package/components/{tryghost-package-json-5.111.0.tgz → tryghost-package-json-5.113.0.tgz} +0 -0
- package/components/tryghost-post-events-5.113.0.tgz +0 -0
- package/components/tryghost-post-revisions-5.113.0.tgz +0 -0
- package/components/{tryghost-posts-service-5.111.0.tgz → tryghost-posts-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-prometheus-metrics-5.111.0.tgz → tryghost-prometheus-metrics-5.113.0.tgz} +0 -0
- package/components/tryghost-recommendations-5.113.0.tgz +0 -0
- package/components/tryghost-referrers-5.113.0.tgz +0 -0
- package/components/{tryghost-security-5.111.0.tgz → tryghost-security-5.113.0.tgz} +0 -0
- package/components/tryghost-session-service-5.113.0.tgz +0 -0
- package/components/{tryghost-settings-path-manager-5.111.0.tgz → tryghost-settings-path-manager-5.113.0.tgz} +0 -0
- package/components/{tryghost-slack-notifications-5.111.0.tgz → tryghost-slack-notifications-5.113.0.tgz} +0 -0
- package/components/{tryghost-tiers-5.111.0.tgz → tryghost-tiers-5.113.0.tgz} +0 -0
- package/components/tryghost-version-notifications-data-service-5.113.0.tgz +0 -0
- package/components/{tryghost-webmentions-5.111.0.tgz → tryghost-webmentions-5.113.0.tgz} +0 -0
- package/core/boot.js +0 -3
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +13043 -11763
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-1298238e.mjs → CodeEditorView-ed5e87be.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{index-0f51ccb5.mjs → index-0ee4d13c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-2707471f.mjs → index-9c7da716.mjs} +20224 -20178
- package/core/built/admin/assets/admin-x-settings/{modals-f5983704.mjs → modals-7708d510.mjs} +2227 -2216
- package/core/built/admin/assets/{chunk.524.405c43b2cb20553b51d9.js → chunk.524.4f0aeb6b611079e528f5.js} +7 -7
- package/core/built/admin/assets/{chunk.582.eb4b096f29c97c9d6a64.js → chunk.582.485df00698ed27a0668b.js} +8 -8
- package/core/built/admin/assets/{ghost-87cffc153ec73d217c1ae9f9207ea5e1.js → ghost-ebf07ae7768b6e9fb9a4b173b6917782.js} +72 -70
- package/core/built/admin/assets/img/ap-nodes-01ee317529e6353a1c34a062c388f1e7.png +0 -0
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12944 -12924
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +136 -134
- package/core/built/admin/assets/posts/posts.js +2 -2
- package/core/built/admin/assets/{vendor-fca15534b8426c0567400113c63a3e21.js → vendor-68a4aa424a179a90f5bbc2b750def576.js} +28 -26
- package/core/built/admin/index.html +4 -4
- package/core/frontend/helpers/get.js +2 -3
- package/core/frontend/services/routing/registry.js +6 -6
- package/core/frontend/src/admin-auth/message-handler.js +1 -1
- package/core/frontend/src/cards/css/cta.css +38 -36
- package/core/server/adapters/cache/AdapterCacheMemoryTTL.js +54 -0
- package/core/server/adapters/cache/memory-ttl.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/settings.js +2 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +2 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +2 -1
- package/core/server/data/migrations/versions/5.112/2025-03-10-10-01-01-add-require-mfa-setting.js +8 -0
- package/core/server/data/migrations/versions/5.113/2025-03-07-12-24-00-add-super-editor.js +31 -0
- package/core/server/data/migrations/versions/5.113/2025-03-07-12-25-00-add-member-perms-to-super-editor.js +291 -0
- package/core/server/data/schema/default-settings/default-settings.json +6 -0
- package/core/server/data/schema/fixtures/fixtures.json +27 -0
- package/core/server/models/invite.js +6 -7
- package/core/server/models/post.js +3 -9
- package/core/server/models/relations/authors.js +2 -4
- package/core/server/models/role-utils.js +38 -0
- package/core/server/models/role.js +7 -5
- package/core/server/models/user.js +41 -28
- package/core/server/services/activitypub/ActivityPubService.js +116 -0
- package/core/server/services/activitypub/ActivityPubService.ts +139 -0
- package/core/server/services/activitypub/ActivityPubServiceWrapper.js +1 -1
- package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +13 -0
- package/core/server/services/email-analytics/lib/queries.js +3 -3
- package/core/server/services/media-inliner/ExternalMediaInliner.js +346 -0
- package/core/server/services/media-inliner/service.js +1 -1
- package/core/server/services/permissions/can-this.js +3 -2
- package/core/server/services/stats/MembersStatsService.js +167 -0
- package/core/server/services/stats/MrrStatsService.js +161 -0
- package/core/server/services/stats/ReferrersStatsService.js +164 -0
- package/core/server/services/stats/StatsService.js +63 -0
- package/core/server/services/stats/SubscriptionStatsService.js +180 -0
- package/core/server/services/stats/service.js +1 -1
- package/core/server/services/url/Resources.js +20 -30
- package/core/server/services/url/UrlService.js +2 -12
- package/core/server/services/url/Urls.js +17 -33
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +2 -1
- package/core/shared/settings-cache/CacheManager.js +4 -4
- package/package.json +139 -142
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +40 -82
- package/components/tryghost-activitypub-5.111.0.tgz +0 -0
- package/components/tryghost-adapter-cache-memory-ttl-5.111.0.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.111.0.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.111.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.111.0.tgz +0 -0
- package/components/tryghost-constants-5.111.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.111.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.111.0.tgz +0 -0
- package/components/tryghost-domain-events-5.111.0.tgz +0 -0
- package/components/tryghost-donations-5.111.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.111.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.111.0.tgz +0 -0
- package/components/tryghost-email-events-5.111.0.tgz +0 -0
- package/components/tryghost-email-service-5.111.0.tgz +0 -0
- package/components/tryghost-external-media-inliner-5.111.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.111.0.tgz +0 -0
- package/components/tryghost-ghost-5.111.0.tgz +0 -0
- package/components/tryghost-i18n-5.111.0.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.111.0.tgz +0 -0
- package/components/tryghost-job-manager-5.111.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.111.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.111.0.tgz +0 -0
- package/components/tryghost-mail-events-5.111.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.111.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.111.0.tgz +0 -0
- package/components/tryghost-member-events-5.111.0.tgz +0 -0
- package/components/tryghost-members-payments-5.111.0.tgz +0 -0
- package/components/tryghost-milestones-5.111.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.111.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.111.0.tgz +0 -0
- package/components/tryghost-post-events-5.111.0.tgz +0 -0
- package/components/tryghost-post-revisions-5.111.0.tgz +0 -0
- package/components/tryghost-recommendations-5.111.0.tgz +0 -0
- package/components/tryghost-referrers-5.111.0.tgz +0 -0
- package/components/tryghost-session-service-5.111.0.tgz +0 -0
- package/components/tryghost-stats-service-5.111.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.111.0.tgz +0 -0
- package/core/demo.js +0 -6
- 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 ||
|
|
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
|
-
|
|
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 (
|
|
83
|
-
checkAgainst = ['Owner', 'Administrator', 'Editor', 'Author', 'Contributor'];
|
|
84
|
-
} else if (
|
|
85
|
-
checkAgainst = ['Administrator', 'Editor', 'Author', 'Contributor'];
|
|
86
|
-
} else if (
|
|
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 (
|
|
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 (
|
|
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') ||
|
|
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 =
|
|
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 =
|
|
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 =
|
|
463
|
+
options.columns = options.columns.filter(col => this.prototype.permittedAttributes().includes(col));
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
delete data.role;
|
|
467
|
-
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 =
|
|
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 =
|
|
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 =
|
|
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'},
|
|
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 (
|
|
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 (
|
|
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 (
|
|
809
|
+
if (typeof userModelOrId === 'number' || typeof userModelOrId === 'string') {
|
|
794
810
|
// Grab the original args without the first one
|
|
795
|
-
origArgs =
|
|
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 =
|
|
832
|
-
} else if (
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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
|
+
}
|
|
@@ -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 {'
|
|
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 === '
|
|
114
|
-
const status = field === '
|
|
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({
|