Merge pull request #1650 from symonbaikov/feature/webhook-scope-filter
feat: filter webhooks per project, board, and account
This commit is contained in:
@@ -22,6 +22,21 @@
|
||||
* - name
|
||||
* - url
|
||||
* properties:
|
||||
* projectId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional project scope
|
||||
* example: "1357158568008091264"
|
||||
* boardId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional board scope
|
||||
* example: "1357158568008091264"
|
||||
* userId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional acting-user scope
|
||||
* example: "1357158568008091264"
|
||||
* name:
|
||||
* type: string
|
||||
* maxLength: 128
|
||||
@@ -72,6 +87,7 @@
|
||||
*/
|
||||
|
||||
const { isUrl } = require('../../../utils/validators');
|
||||
const { idInput } = require('../../../utils/inputs');
|
||||
|
||||
const Errors = {
|
||||
LIMIT_REACHED: {
|
||||
@@ -81,6 +97,18 @@ const Errors = {
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
projectId: {
|
||||
...idInput,
|
||||
allowNull: true,
|
||||
},
|
||||
boardId: {
|
||||
...idInput,
|
||||
allowNull: true,
|
||||
},
|
||||
userId: {
|
||||
...idInput,
|
||||
allowNull: true,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
maxLength: 128,
|
||||
@@ -121,7 +149,7 @@ module.exports = {
|
||||
async fn(inputs) {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const values = _.pick(inputs, ['name', 'url', 'accessToken']);
|
||||
const values = _.pick(inputs, ['projectId', 'boardId', 'userId', 'name', 'url', 'accessToken']);
|
||||
const events = inputs.events && inputs.events.split(',');
|
||||
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
|
||||
|
||||
|
||||
@@ -27,6 +27,21 @@
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* projectId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional project scope
|
||||
* example: "1357158568008091264"
|
||||
* boardId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional board scope
|
||||
* example: "1357158568008091264"
|
||||
* userId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional acting-user scope
|
||||
* example: "1357158568008091264"
|
||||
* name:
|
||||
* type: string
|
||||
* maxLength: 128
|
||||
@@ -91,6 +106,18 @@ module.exports = {
|
||||
...idInput,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
...idInput,
|
||||
allowNull: true,
|
||||
},
|
||||
boardId: {
|
||||
...idInput,
|
||||
allowNull: true,
|
||||
},
|
||||
userId: {
|
||||
...idInput,
|
||||
allowNull: true,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
@@ -136,7 +163,7 @@ module.exports = {
|
||||
throw Errors.WEBHOOK_NOT_FOUND;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['name', 'url', 'accessToken']);
|
||||
const values = _.pick(inputs, ['projectId', 'boardId', 'userId', 'name', 'url', 'accessToken']);
|
||||
const events = inputs.events && inputs.events.split(',');
|
||||
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
|
||||
|
||||
|
||||
@@ -7,6 +7,48 @@ const { ProxyAgent } = require('undici');
|
||||
|
||||
const Webhook = require('../../models/Webhook');
|
||||
|
||||
const BOARD_ITEM_EVENTS = new Set([
|
||||
Webhook.Events.BOARD_CREATE,
|
||||
Webhook.Events.BOARD_UPDATE,
|
||||
Webhook.Events.BOARD_DELETE,
|
||||
]);
|
||||
|
||||
const PROJECT_ITEM_EVENTS = new Set([
|
||||
Webhook.Events.PROJECT_CREATE,
|
||||
Webhook.Events.PROJECT_UPDATE,
|
||||
Webhook.Events.PROJECT_DELETE,
|
||||
]);
|
||||
|
||||
function resolveScope(event, data, override) {
|
||||
const result = { projectId: null, boardId: null, ...(override || {}) };
|
||||
if (!data) return result;
|
||||
|
||||
const { item } = data;
|
||||
const included = data.included || {};
|
||||
|
||||
if (!result.projectId) {
|
||||
if (included.projects && included.projects[0]) {
|
||||
result.projectId = included.projects[0].id;
|
||||
} else if (item && PROJECT_ITEM_EVENTS.has(event)) {
|
||||
result.projectId = item.id;
|
||||
} else if (item && item.projectId) {
|
||||
result.projectId = item.projectId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.boardId) {
|
||||
if (included.boards && included.boards[0]) {
|
||||
result.boardId = included.boards[0].id;
|
||||
} else if (item && BOARD_ITEM_EVENTS.has(event)) {
|
||||
result.boardId = item.id;
|
||||
} else if (item && item.boardId) {
|
||||
result.boardId = item.boardId;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Included
|
||||
* @property {any[]} [users] - Array of users (optional).
|
||||
@@ -104,10 +146,15 @@ module.exports = {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
scope: {
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const webhooks = inputs.webhooks.filter((webhook) => {
|
||||
const userId = inputs.user && inputs.user.id;
|
||||
|
||||
const eventFilteredWebhooks = inputs.webhooks.filter((webhook) => {
|
||||
if (!webhook.url) {
|
||||
return false;
|
||||
}
|
||||
@@ -120,6 +167,31 @@ module.exports = {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (webhook.userId && webhook.userId !== userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (eventFilteredWebhooks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = inputs.buildData();
|
||||
const prevData = inputs.buildPrevData && inputs.buildPrevData();
|
||||
|
||||
const scope = resolveScope(inputs.event, data, inputs.scope);
|
||||
|
||||
const webhooks = eventFilteredWebhooks.filter((webhook) => {
|
||||
if (webhook.projectId && webhook.projectId !== scope.projectId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (webhook.boardId && webhook.boardId !== scope.boardId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -127,9 +199,6 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = inputs.buildData();
|
||||
const prevData = inputs.buildPrevData && inputs.buildPrevData();
|
||||
|
||||
webhooks.forEach((webhook) => {
|
||||
sendWebhook(
|
||||
webhook,
|
||||
|
||||
@@ -30,6 +30,21 @@
|
||||
* type: string
|
||||
* description: Unique identifier for the webhook
|
||||
* example: "1357158568008091264"
|
||||
* projectId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional project scope; webhook fires only for events within this project
|
||||
* example: "1357158568008091264"
|
||||
* boardId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional board scope; webhook fires only for events within this board
|
||||
* example: "1357158568008091264"
|
||||
* userId:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Optional acting-user scope; webhook fires only for events triggered by this user
|
||||
* example: "1357158568008091264"
|
||||
* name:
|
||||
* type: string
|
||||
* description: Name/title of the webhook
|
||||
@@ -200,9 +215,17 @@ module.exports = {
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
|
||||
projectId: {
|
||||
model: 'Project',
|
||||
columnName: 'project_id',
|
||||
},
|
||||
boardId: {
|
||||
model: 'Board',
|
||||
columnName: 'board_id',
|
||||
},
|
||||
userId: {
|
||||
model: 'User',
|
||||
columnName: 'user_id',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
exports.up = (knex) =>
|
||||
knex.schema.alterTable('webhook', (table) => {
|
||||
table.bigInteger('project_id');
|
||||
table.bigInteger('user_id');
|
||||
|
||||
table.index('project_id');
|
||||
table.index('user_id');
|
||||
});
|
||||
|
||||
exports.down = (knex) =>
|
||||
knex.schema.alterTable('webhook', (table) => {
|
||||
table.dropIndex('user_id');
|
||||
table.dropIndex('project_id');
|
||||
|
||||
table.dropColumn('user_id');
|
||||
table.dropColumn('project_id');
|
||||
});
|
||||
Reference in New Issue
Block a user