1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 12:49:43 +02:00

feat: Events via webhook (#771)

Closes #215, closes #656
This commit is contained in:
HannesOberreiter 2024-06-06 20:22:14 +02:00 committed by GitHub
parent c0b694039e
commit 193daf6cfb
38 changed files with 416 additions and 9 deletions

View file

@ -46,6 +46,9 @@ SECRET_KEY=notsecretkey
# SLACK_BOT_TOKEN= # SLACK_BOT_TOKEN=
# SLACK_CHANNEL_ID= # SLACK_CHANNEL_ID=
# WEBHOOK_URL=
# WEBHOOK_BEARER=
## Do not edit this ## Do not edit this
TZ=UTC TZ=UTC

View file

@ -44,7 +44,7 @@ module.exports = {
async fn(inputs, exits) { async fn(inputs, exits) {
const { currentUser } = this.req; const { currentUser } = this.req;
const { card } = await sails.helpers.cards const { card, board } = await sails.helpers.cards
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
@ -88,6 +88,7 @@ module.exports = {
card, card,
creatorUser: currentUser, creatorUser: currentUser,
}, },
board,
requestId: inputs.requestId, requestId: inputs.requestId,
request: this.req, request: this.req,
}); });

View file

@ -45,7 +45,7 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { currentUser } = this.req; const { currentUser } = this.req;
const { card } = await sails.helpers.cards const { card, board } = await sails.helpers.cards
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
@ -74,6 +74,7 @@ module.exports = {
card, card,
userId: inputs.userId, userId: inputs.userId,
}, },
board,
request: this.req, request: this.req,
}) })
.intercept('userAlreadyCardMember', () => Errors.USER_ALREADY_CARD_MEMBER); .intercept('userAlreadyCardMember', () => Errors.USER_ALREADY_CARD_MEMBER);

View file

@ -39,7 +39,7 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { currentUser } = this.req; const { currentUser } = this.req;
const { board } = await sails.helpers.cards const { board, card } = await sails.helpers.cards
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
@ -67,6 +67,7 @@ module.exports = {
cardMembership = await sails.helpers.cardMemberships.deleteOne.with({ cardMembership = await sails.helpers.cardMemberships.deleteOne.with({
board, board,
card,
record: cardMembership, record: cardMembership,
request: this.req, request: this.req,
}); });

View file

@ -36,7 +36,7 @@ module.exports = {
.intercept('pathNotFound', () => Errors.COMMENT_ACTION_NOT_FOUND); .intercept('pathNotFound', () => Errors.COMMENT_ACTION_NOT_FOUND);
let { action } = path; let { action } = path;
const { board, project } = path; const { board, project, card } = path;
const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id);
@ -61,6 +61,7 @@ module.exports = {
action = await sails.helpers.actions.deleteOne.with({ action = await sails.helpers.actions.deleteOne.with({
board, board,
card,
record: action, record: action,
request: this.req, request: this.req,
}); });

View file

@ -40,7 +40,7 @@ module.exports = {
.intercept('pathNotFound', () => Errors.COMMENT_ACTION_NOT_FOUND); .intercept('pathNotFound', () => Errors.COMMENT_ACTION_NOT_FOUND);
let { action } = path; let { action } = path;
const { board, project } = path; const { board, project, card } = path;
const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id);
@ -69,6 +69,7 @@ module.exports = {
action = await sails.helpers.actions.updateOne.with({ action = await sails.helpers.actions.updateOne.with({
values, values,
card,
board, board,
record: action, record: action,
request: this.req, request: this.req,

View file

@ -28,7 +28,8 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { currentUser } = this.req; const { currentUser } = this.req;
let { list } = await sails.helpers.lists // eslint-disable-next-line prefer-const
let { list, board } = await sails.helpers.lists
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
@ -47,6 +48,7 @@ module.exports = {
list = await sails.helpers.lists.deleteOne.with({ list = await sails.helpers.lists.deleteOne.with({
record: list, record: list,
board,
request: this.req, request: this.req,
}); });

View file

@ -35,7 +35,8 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { currentUser } = this.req; const { currentUser } = this.req;
let { list } = await sails.helpers.lists // eslint-disable-next-line prefer-const
let { list, board } = await sails.helpers.lists
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
@ -56,6 +57,7 @@ module.exports = {
list = await sails.helpers.lists.updateOne.with({ list = await sails.helpers.lists.updateOne.with({
values, values,
board,
record: list, record: list,
request: this.req, request: this.req,
}); });

View file

@ -39,7 +39,7 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { currentUser } = this.req; const { currentUser } = this.req;
const { card } = await sails.helpers.cards const { card, board } = await sails.helpers.cards
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
@ -63,6 +63,7 @@ module.exports = {
...values, ...values,
card, card,
}, },
board,
request: this.req, request: this.req,
}); });

View file

@ -95,6 +95,15 @@ module.exports = {
buildAndSendSlackMessage(values.user, values.card, action); buildAndSendSlackMessage(values.user, values.card, action);
} }
await sails.helpers.utils.sendWebhook.with({
event: 'ACTION_CREATE',
data: action,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: values.card,
board: inputs.board,
});
return action; return action;
}, },
}; };

View file

@ -4,6 +4,10 @@ module.exports = {
type: 'ref', type: 'ref',
required: true, required: true,
}, },
card: {
type: 'ref',
required: true,
},
board: { board: {
type: 'ref', type: 'ref',
required: true, required: true,
@ -25,6 +29,15 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'ACTION_DELETE',
data: action,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: inputs.card,
board: inputs.board,
});
} }
return action; return action;

View file

@ -8,6 +8,10 @@ module.exports = {
type: 'json', type: 'json',
required: true, required: true,
}, },
card: {
type: 'ref',
required: true,
},
board: { board: {
type: 'ref', type: 'ref',
required: true, required: true,
@ -31,6 +35,15 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'ACTION_UPDATE',
data: action,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: inputs.card,
board: inputs.board,
});
} }
return action; return action;

View file

@ -21,6 +21,10 @@ module.exports = {
custom: valuesValidator, custom: valuesValidator,
required: true, required: true,
}, },
board: {
type: 'ref',
required: true,
},
requestId: { requestId: {
type: 'string', type: 'string',
isNotEmptyString: true, isNotEmptyString: true,
@ -31,7 +35,7 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
const { values } = inputs; const { values, board } = inputs;
const attachment = await Attachment.create({ const attachment = await Attachment.create({
...values, ...values,
@ -55,9 +59,20 @@ module.exports = {
values: { values: {
coverAttachmentId: attachment.id, coverAttachmentId: attachment.id,
}, },
board,
request: inputs.request,
}); });
} }
await sails.helpers.utils.sendWebhook.with({
event: 'ATTACHMENT_CREATE',
data: attachment,
projectId: board.projectId,
user: inputs.request.currentUser,
card: values.card,
board,
});
return attachment; return attachment;
}, },
}; };

View file

@ -48,6 +48,15 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'ATTACHMENT_DELETE',
data: attachment,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: inputs.card,
board: inputs.board,
});
} }
return attachment; return attachment;

View file

@ -31,6 +31,14 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'ATTACHMENT_UPDATE',
data: attachment,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
board: inputs.board,
});
} }
return attachment; return attachment;

View file

@ -111,6 +111,14 @@ module.exports = {
); );
}); });
await sails.helpers.utils.sendWebhook.with({
event: 'BOARD_CREATE',
data: board,
projectId: board.projectId,
user: inputs.request.currentUser,
board,
});
return { return {
board, board,
boardMembership, boardMembership,

View file

@ -35,6 +35,14 @@ module.exports = {
}); });
} }
await sails.helpers.utils.sendWebhook.with({
event: 'BOARD_DELETE',
data: board,
projectId: board.projectId,
user: inputs.request.currentUser,
board,
});
return board; return board;
}, },
}; };

View file

@ -83,6 +83,14 @@ module.exports = {
}); });
} }
await sails.helpers.utils.sendWebhook.with({
event: 'BOARD_UPDATE',
data: board,
projectId: board.projectId,
user: inputs.request.currentUser,
board,
});
return board; return board;
}, },
}; };

View file

@ -21,6 +21,10 @@ module.exports = {
custom: valuesValidator, custom: valuesValidator,
required: true, required: true,
}, },
board: {
type: 'ref',
required: true,
},
request: { request: {
type: 'ref', type: 'ref',
}, },
@ -75,6 +79,15 @@ module.exports = {
); );
} }
await sails.helpers.utils.sendWebhook.with({
event: 'CARD_MEMBERSHIP_CREATE',
data: cardMembership,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: values.card,
board: inputs.board,
});
return cardMembership; return cardMembership;
}, },
}; };

View file

@ -4,6 +4,10 @@ module.exports = {
type: 'ref', type: 'ref',
required: true, required: true,
}, },
card: {
type: 'ref',
required: true,
},
board: { board: {
type: 'ref', type: 'ref',
required: true, required: true,
@ -40,6 +44,15 @@ module.exports = {
}, },
}); });
} }
await sails.helpers.utils.sendWebhook.with({
event: 'CARD_MEMBERSHIP_DELETE',
data: cardMembership,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: inputs.card,
board: inputs.board,
});
} }
return cardMembership; return cardMembership;

View file

@ -109,6 +109,17 @@ module.exports = {
user: values.creatorUser, user: values.creatorUser,
}, },
board: inputs.board, board: inputs.board,
request: inputs.request,
});
await sails.helpers.utils.sendWebhook.with({
event: 'CARD_CREATE',
data: card,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card,
board: inputs.board,
list: values.list,
}); });
return card; return card;

View file

@ -21,6 +21,10 @@ module.exports = {
const card = await Card.archiveOne(inputs.record.id); const card = await Card.archiveOne(inputs.record.id);
if (card) { if (card) {
const { board } = await sails.helpers.lists
.getProjectPath(card.listId)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
sails.sockets.broadcast( sails.sockets.broadcast(
`board:${card.boardId}`, `board:${card.boardId}`,
'cardDelete', 'cardDelete',
@ -33,6 +37,15 @@ module.exports = {
if (sails.config.custom.slackBotToken) { if (sails.config.custom.slackBotToken) {
buildAndSendSlackMessage(inputs.user, card); buildAndSendSlackMessage(inputs.user, card);
} }
await sails.helpers.utils.sendWebhook.with({
event: 'CARD_DELETE',
data: card,
projectId: board.projectId,
user: inputs.request.currentUser,
card,
board,
});
} }
return card; return card;

View file

@ -132,6 +132,17 @@ module.exports = {
user: values.creatorUser, user: values.creatorUser,
}, },
board: inputs.board, board: inputs.board,
request: inputs.request,
});
await sails.helpers.utils.sendWebhook.with({
event: 'CARD_CREATE',
data: card,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card,
board: inputs.board,
list: inputs.list,
}); });
return { return {

View file

@ -233,6 +233,7 @@ module.exports = {
}, },
}, },
board: inputs.board, board: inputs.board,
request: inputs.request,
}); });
} }
@ -269,6 +270,15 @@ module.exports = {
} }
} }
await sails.helpers.utils.sendWebhook.with({
event: 'CARD_UPDATE',
data: card,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card,
board: inputs.board,
});
return card; return card;
}, },
}; };

View file

@ -67,6 +67,14 @@ module.exports = {
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'LIST_CREATE',
data: list,
projectId: values.board.projectId,
user: inputs.request.currentUser,
board: values.board,
});
return list; return list;
}, },
}; };

View file

@ -4,6 +4,10 @@ module.exports = {
type: 'ref', type: 'ref',
required: true, required: true,
}, },
board: {
type: 'ref',
required: true,
},
request: { request: {
type: 'ref', type: 'ref',
}, },
@ -21,6 +25,14 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'LIST_DELETE',
data: list,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
board: inputs.board,
});
} }
return list; return list;

View file

@ -21,6 +21,10 @@ module.exports = {
custom: valuesValidator, custom: valuesValidator,
required: true, required: true,
}, },
board: {
type: 'ref',
required: true,
},
request: { request: {
type: 'ref', type: 'ref',
}, },
@ -67,6 +71,14 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'LIST_UPDATE',
data: list,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
board: inputs.board,
});
} }
return list; return list;

View file

@ -32,6 +32,13 @@ module.exports = {
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'PROJECT_CREATE',
data: project,
projectId: project.id,
user: inputs.request.currentUser,
});
return { return {
project, project,
projectManager, projectManager,

View file

@ -37,6 +37,13 @@ module.exports = {
inputs.request, inputs.request,
); );
}); });
await sails.helpers.utils.sendWebhook.with({
event: 'PROJECT_DELETE',
data: project,
projectId: project.id,
user: inputs.request.currentUser,
});
} }
return project; return project;

View file

@ -108,6 +108,13 @@ module.exports = {
inputs.request, inputs.request,
); );
}); });
await sails.helpers.utils.sendWebhook.with({
event: 'PROJECT_UPDATE',
data: project,
projectId: project.id,
user: inputs.request.currentUser,
});
} }
return project; return project;

View file

@ -21,6 +21,10 @@ module.exports = {
custom: valuesValidator, custom: valuesValidator,
required: true, required: true,
}, },
board: {
type: 'ref',
required: true,
},
request: { request: {
type: 'ref', type: 'ref',
}, },
@ -67,6 +71,15 @@ module.exports = {
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'TASK_CREATE',
data: task,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: values.card,
board: inputs.board,
});
return task; return task;
}, },
}; };

View file

@ -25,6 +25,14 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'TASK_DELETE',
data: task,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
board: inputs.board,
});
} }
return task; return task;

View file

@ -71,6 +71,15 @@ module.exports = {
}, },
inputs.request, inputs.request,
); );
await sails.helpers.utils.sendWebhook.with({
event: 'TASK_UPDATE',
data: task,
projectId: inputs.board.projectId,
user: inputs.request.currentUser,
card: values.card,
board: inputs.board,
});
} }
return task; return task;

View file

@ -84,6 +84,23 @@ module.exports = {
); );
}); });
/* The user could be created manually by an user or via OIDC. We hijack the id field, so one can differentiate between the two on the webhook side. */
let initiator;
if (inputs.request && inputs.request.currentUser) {
initiator = inputs.request.currentUser;
} else {
initiator = {
id: 'oidc',
};
}
await sails.helpers.utils.sendWebhook.with({
event: 'USER_CREATE',
data: { ...user, password: undefined },
projectId: '',
user: initiator,
});
return user; return user;
}, },
}; };

View file

@ -59,6 +59,13 @@ module.exports = {
inputs.request, inputs.request,
); );
}); });
await sails.helpers.utils.sendWebhook.with({
event: 'USER_DELETE',
data: { ...user, password: undefined },
projectId: '',
user: inputs.request.currentUser,
});
} }
return user; return user;

View file

@ -154,6 +154,13 @@ module.exports = {
); );
}); });
} }
await sails.helpers.utils.sendWebhook.with({
event: 'USER_UPDATE',
data: { ...user, password: undefined },
projectId: '',
user: inputs.request.currentUser,
});
} }
return user; return user;

View file

@ -0,0 +1,115 @@
const EVENT_TYPES = {
ACTION_CREATE: 'action_create',
ACTION_UPDATE: 'action_update',
ACTION_DELETE: 'action_delete',
CARD_CREATE: 'card_create',
CARD_UPDATE: 'card_update',
CARD_DELETE: 'card_delete',
CARD_MEMBERSHIP_CREATE: 'card_membership_create',
CARD_MEMBERSHIP_DELETE: 'card_membership_delete',
LIST_CREATE: 'list_create',
LIST_UPDATE: 'list_update',
LIST_DELETE: 'list_delete',
BOARD_CREATE: 'board_create',
BOARD_UPDATE: 'board_update',
BOARD_DELETE: 'board_delete',
ATTACHMENT_CREATE: 'attachment_create',
ATTACHMENT_UPDATE: 'attachment_update',
ATTACHMENT_DELETE: 'attachment_delete',
PROJECT_CREATE: 'project_create',
PROJECT_UPDATE: 'project_update',
PROJECT_DELETE: 'project_delete',
TASK_CREATE: 'task_create',
TASK_UPDATE: 'task_update',
TASK_DELETE: 'task_delete',
USER_CREATE: 'user_create',
USER_UPDATE: 'user_update',
USER_DELETE: 'user_delete',
};
/**
* Sends a webhook notification to a configured URL.
*
* @param {Object} inputs - Data to include in the webhook payload.
* @param {string} inputs.event - The event type (see {@link EVENT_TYPES}).
* @param {*} inputs.data - The actual data related to the event.
* @param {string} inputs.projectId - The project ID associated with the event.
* @param {ref} [inputs.user] - Optional user object associated with the event.
* @param {ref} [inputs.card] - Optional card object associated with the event.
* @param {ref} [inputs.board] - Optional board object associated with the event.
* @param {ref} [inputs.list] - Optional list object associated with the event.
* @returns {Promise<void>}
*/
async function sendWebhook(inputs) {
const url = sails.config.custom.webhookUrl;
const headers = {
'Content-Type': 'application/json',
};
if (sails.config.custom.webhookBearer) {
headers.Authorization = `Bearer ${sails.config.custom.webhookBearer}`;
}
const body = JSON.stringify({
...inputs,
user: {
...inputs.user,
password: undefined,
},
});
const req = await fetch(url, {
method: 'POST',
headers,
body,
});
if (req.status !== 200) {
sails.log.error(`Webhook failed with status ${req.status} and message: ${await req.text()}`);
}
}
module.exports = {
eventTypes: EVENT_TYPES,
inputs: {
event: {
type: 'string',
isIn: Object.keys(EVENT_TYPES),
required: true,
},
data: {
type: 'ref',
required: true,
},
projectId: {
type: 'string',
default: '',
},
user: {
type: 'ref',
},
card: {
type: 'ref',
},
board: {
type: 'ref',
},
list: {
type: 'ref',
},
},
async fn(inputs) {
if (!sails.config.custom.webhookUrl) return;
try {
await sendWebhook(inputs);
} catch (err) {
sails.log.error(err);
}
},
};

View file

@ -62,4 +62,7 @@ module.exports.custom = {
slackBotToken: process.env.SLACK_BOT_TOKEN, slackBotToken: process.env.SLACK_BOT_TOKEN,
slackChannelId: process.env.SLACK_CHANNEL_ID, slackChannelId: process.env.SLACK_CHANNEL_ID,
webhookUrl: process.env.WEBHOOK_URL,
webhookBearer: process.env.WEBHOOK_BEARER,
}; };