feat: filter webhooks per project, board, and account

Adds optional project, board, and acting-user scope to each webhook so
events from a specific board or project can be routed to a single
endpoint without an external automation tool in the middle (closes
#1457).

A webhook now fires for an event only when every set scope matches:
empty scope still means "fire for everything", keeping existing
webhooks working unchanged. Scope filtering is centralized in the
sendWebhooks helper, which auto-derives projectId/boardId from the
event payload, so existing call sites are untouched.
This commit is contained in:
Symon
2026-05-02 10:31:30 +03:00
parent a8dcd7cef3
commit 05979a460f
12 changed files with 446 additions and 131 deletions
+29 -1
View File
@@ -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(',');
+28 -1
View File
@@ -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(',');