diff --git a/client/src/actions/core.js b/client/src/actions/core.js index 8f87fe82..aebc9174 100644 --- a/client/src/actions/core.js +++ b/client/src/actions/core.js @@ -8,6 +8,7 @@ import ActionTypes from '../constants/ActionTypes'; const initializeCore = ( user, board, + webhooks, users, projects, projectManagers, @@ -33,6 +34,7 @@ const initializeCore = ( payload: { user, board, + webhooks, users, projects, projectManagers, diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 626d23c6..46a254a1 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -8,6 +8,7 @@ import socket from './socket'; import login from './login'; import core from './core'; import modals from './modals'; +import webhooks from './webhooks'; import users from './users'; import projects from './projects'; import projectManagers from './project-managers'; @@ -35,6 +36,7 @@ export default { ...login, ...core, ...modals, + ...webhooks, ...users, ...projects, ...projectManagers, diff --git a/client/src/actions/socket.js b/client/src/actions/socket.js index 65881221..8e6f07f4 100644 --- a/client/src/actions/socket.js +++ b/client/src/actions/socket.js @@ -14,6 +14,7 @@ const handleSocketReconnect = ( config, user, board, + webhooks, users, projects, projectManagers, @@ -40,6 +41,7 @@ const handleSocketReconnect = ( config, user, board, + webhooks, users, projects, projectManagers, diff --git a/client/src/actions/users.js b/client/src/actions/users.js index bdf00bde..101d65c6 100644 --- a/client/src/actions/users.js +++ b/client/src/actions/users.js @@ -67,6 +67,7 @@ const handleUserUpdate = ( boardIds, config, board, + webhooks, users, projects, projectManagers, @@ -95,6 +96,7 @@ const handleUserUpdate = ( boardIds, config, board, + webhooks, users, projects, projectManagers, diff --git a/client/src/actions/webhooks.js b/client/src/actions/webhooks.js new file mode 100644 index 00000000..41e35dbe --- /dev/null +++ b/client/src/actions/webhooks.js @@ -0,0 +1,104 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import ActionTypes from '../constants/ActionTypes'; + +const createWebhook = (webhook) => ({ + type: ActionTypes.WEBHOOK_CREATE, + payload: { + webhook, + }, +}); + +createWebhook.success = (localId, webhook) => ({ + type: ActionTypes.WEBHOOK_CREATE__SUCCESS, + payload: { + localId, + webhook, + }, +}); + +createWebhook.failure = (localId, error) => ({ + type: ActionTypes.WEBHOOK_CREATE__FAILURE, + payload: { + localId, + error, + }, +}); + +const handleWebhookCreate = (webhook) => ({ + type: ActionTypes.WEBHOOK_CREATE_HANDLE, + payload: { + webhook, + }, +}); + +const updateWebhook = (id, data) => ({ + type: ActionTypes.WEBHOOK_UPDATE, + payload: { + id, + data, + }, +}); + +updateWebhook.success = (webhook) => ({ + type: ActionTypes.WEBHOOK_UPDATE__SUCCESS, + payload: { + webhook, + }, +}); + +updateWebhook.failure = (id, error) => ({ + type: ActionTypes.WEBHOOK_UPDATE__FAILURE, + payload: { + id, + error, + }, +}); + +const handleWebhookUpdate = (webhook) => ({ + type: ActionTypes.WEBHOOK_UPDATE_HANDLE, + payload: { + webhook, + }, +}); + +const deleteWebhook = (id) => ({ + type: ActionTypes.WEBHOOK_DELETE, + payload: { + id, + }, +}); + +deleteWebhook.success = (webhook) => ({ + type: ActionTypes.WEBHOOK_DELETE__SUCCESS, + payload: { + webhook, + }, +}); + +deleteWebhook.failure = (id, error) => ({ + type: ActionTypes.WEBHOOK_DELETE__FAILURE, + payload: { + id, + error, + }, +}); + +const handleWebhookDelete = (webhook) => ({ + type: ActionTypes.WEBHOOK_DELETE_HANDLE, + payload: { + webhook, + }, +}); + +export default { + createWebhook, + handleWebhookCreate, + updateWebhook, + handleWebhookUpdate, + deleteWebhook, + handleWebhookDelete, +}; diff --git a/client/src/api/index.js b/client/src/api/index.js index ca98d773..70a1bb4c 100755 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -7,6 +7,7 @@ import http from './http'; import socket from './socket'; import config from './config'; import accessTokens from './access-tokens'; +import webhooks from './webhooks'; import users from './users'; import projects from './projects'; import projectManagers from './project-managers'; @@ -35,6 +36,7 @@ export { http, socket }; export default { ...config, ...accessTokens, + ...webhooks, ...users, ...projects, ...projectManagers, diff --git a/client/src/api/webhooks.js b/client/src/api/webhooks.js new file mode 100755 index 00000000..ae29ba06 --- /dev/null +++ b/client/src/api/webhooks.js @@ -0,0 +1,23 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import socket from './socket'; + +/* Actions */ + +const getWebhooks = (headers) => socket.get('/webhooks', undefined, headers); + +const createWebhook = (data, headers) => socket.post('/webhooks', data, headers); + +const updateWebhook = (id, data, headers) => socket.patch(`/webhooks/${id}`, data, headers); + +const deleteWebhook = (id, headers) => socket.delete(`/webhooks/${id}`, undefined, headers); + +export default { + getWebhooks, + createWebhook, + updateWebhook, + deleteWebhook, +}; diff --git a/client/src/components/common/AdministrationModal/AdministrationModal.jsx b/client/src/components/common/AdministrationModal/AdministrationModal.jsx index 8dbdcc94..d79d22ec 100644 --- a/client/src/components/common/AdministrationModal/AdministrationModal.jsx +++ b/client/src/components/common/AdministrationModal/AdministrationModal.jsx @@ -12,6 +12,7 @@ import { Modal, Tab } from 'semantic-ui-react'; import entryActions from '../../../entry-actions'; import { useClosableModal } from '../../../hooks'; import UsersPane from './UsersPane'; +import WebhooksPane from './WebhooksPane'; import styles from './AdministrationModal.module.scss'; @@ -37,6 +38,12 @@ const AdministrationModal = React.memo(() => { }), render: () => , }, + { + menuItem: t('common.webhooks', { + context: 'title', + }), + render: () => , + }, ]; const isUsersPaneActive = activeTabIndex === 0; diff --git a/client/src/components/common/AdministrationModal/WebhooksPane.jsx b/client/src/components/common/AdministrationModal/WebhooksPane.jsx new file mode 100644 index 00000000..13f2aaf7 --- /dev/null +++ b/client/src/components/common/AdministrationModal/WebhooksPane.jsx @@ -0,0 +1,35 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Tab } from 'semantic-ui-react'; + +import selectors from '../../../selectors'; +import entryActions from '../../../entry-actions'; +import Webhooks from '../../webhooks/Webhooks'; + +import styles from './WebhooksPane.module.scss'; + +const WebhooksPane = React.memo(() => { + const webhookIds = useSelector(selectors.selectWebhookIds); + + const dispatch = useDispatch(); + + const handleCreate = useCallback( + (data) => { + dispatch(entryActions.createWebhook(data)); + }, + [dispatch], + ); + + return ( + + + + ); +}); + +export default WebhooksPane; diff --git a/client/src/components/common/AdministrationModal/WebhooksPane.module.scss b/client/src/components/common/AdministrationModal/WebhooksPane.module.scss new file mode 100644 index 00000000..e678a8dc --- /dev/null +++ b/client/src/components/common/AdministrationModal/WebhooksPane.module.scss @@ -0,0 +1,11 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +:global(#app) { + .wrapper { + border: none; + box-shadow: none; + } +} diff --git a/client/src/components/common/ConfirmationStep/ConfirmationStep.jsx b/client/src/components/common/ConfirmationStep/ConfirmationStep.jsx index a985adfa..d88ae5c4 100644 --- a/client/src/components/common/ConfirmationStep/ConfirmationStep.jsx +++ b/client/src/components/common/ConfirmationStep/ConfirmationStep.jsx @@ -28,21 +28,26 @@ const ConfirmationStep = React.memo( const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef'); - const handleSubmit = useCallback(() => { - if (typeValue) { - const cleanData = { - ...data, - typeValue: data.typeValue.trim(), - }; + const handleSubmit = useCallback( + (event) => { + event.stopPropagation(); - if (cleanData.typeValue.toLowerCase() !== typeValue.toLowerCase()) { - nameFieldRef.current.select(); - return; + if (typeValue) { + const cleanData = { + ...data, + typeValue: data.typeValue.trim(), + }; + + if (cleanData.typeValue.toLowerCase() !== typeValue.toLowerCase()) { + nameFieldRef.current.select(); + return; + } } - } - onConfirm(); - }, [typeValue, onConfirm, data, nameFieldRef]); + onConfirm(); + }, + [typeValue, onConfirm, data, nameFieldRef], + ); useEffect(() => { if (typeValue) { diff --git a/client/src/components/notification-services/NotificationServices/Item.jsx b/client/src/components/notification-services/NotificationServices/Item.jsx index 13015572..fa8a7ec8 100644 --- a/client/src/components/notification-services/NotificationServices/Item.jsx +++ b/client/src/components/notification-services/NotificationServices/Item.jsx @@ -125,7 +125,7 @@ const Item = React.memo(({ id }) => { dispatch(entryActions.deleteNotificationService(id)); }, [id, dispatch]); - const handleUpdateSubmit = useCallback(() => { + const handleSubmit = useCallback(() => { urlFieldRef.current.blur(); }, [urlFieldRef]); @@ -153,7 +153,7 @@ const Item = React.memo(({ id }) => { const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep); return ( -
+ { + const [t] = useTranslation(); + + const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef'); + const [urlFieldRef, handleUrlFieldRef] = useNestedRef('inputRef'); + + const focusNameField = useCallback(() => { + nameFieldRef.current.focus({ + preventScroll: true, + }); + }, [nameFieldRef]); + + const selectNameField = useCallback(() => { + nameFieldRef.current.select(); + }, [nameFieldRef]); + + const selectUrlField = useCallback(() => { + urlFieldRef.current.select(); + }, [urlFieldRef]); + + useImperativeHandle( + ref, + () => ({ + focusNameField, + selectNameField, + selectUrlField, + }), + [focusNameField, selectNameField, selectUrlField], + ); + + return ( + <> +
{t('common.title')}
+ +
{t('common.url')}
+ +
+ {t('common.accessToken')} ( + {t('common.optional', { + context: 'inline', + })} + ) +
+ + {data.excludedEvents.length === 0 && ( + <> +
+ {t('common.events')} ( + {t('common.optional', { + context: 'inline', + })} + ) +
+ ({ + text: event, + value: event, + }))} + value={data.events} + placeholder="All" + readOnly={isReadOnly} + className={styles.field} + onChange={onFieldChange} + /> + + )} + {data.events.length === 0 && ( + <> +
+ {t('common.excludedEvents')} ( + {t('common.optional', { + context: 'inline', + })} + ) +
+ ({ + text: event, + value: event, + }))} + value={data.excludedEvents} + placeholder="None" + readOnly={isReadOnly} + className={styles.field} + onChange={onFieldChange} + /> + + )} + + ); +}); + +Editor.propTypes = { + data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + isReadOnly: PropTypes.bool, + onFieldChange: PropTypes.func.isRequired, +}; + +Editor.defaultProps = { + isReadOnly: false, +}; + +export default React.memo(Editor); diff --git a/client/src/components/webhooks/Webhooks/Editor.module.scss b/client/src/components/webhooks/Webhooks/Editor.module.scss new file mode 100644 index 00000000..352a33b8 --- /dev/null +++ b/client/src/components/webhooks/Webhooks/Editor.module.scss @@ -0,0 +1,17 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +:global(#app) { + .field { + margin-bottom: 8px; + } + + .text { + color: #444444; + font-size: 12px; + font-weight: bold; + padding-bottom: 6px; + } +} diff --git a/client/src/components/webhooks/Webhooks/Item.jsx b/client/src/components/webhooks/Webhooks/Item.jsx new file mode 100644 index 00000000..dc484912 --- /dev/null +++ b/client/src/components/webhooks/Webhooks/Item.jsx @@ -0,0 +1,137 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import { dequal } from 'dequal'; +import React, { useCallback, useMemo, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { Accordion, Button, Form, Icon } from 'semantic-ui-react'; + +import selectors from '../../../selectors'; +import entryActions from '../../../entry-actions'; +import { useForm, usePopupInClosableContext } from '../../../hooks'; +import { isUrl } from '../../../utils/validator'; +import Editor from './Editor'; +import ConfirmationStep from '../../common/ConfirmationStep'; + +import styles from './Item.module.scss'; +import { useToggle } from '../../../lib/hooks'; + +const Item = React.memo(({ id }) => { + const selectWebhookById = useMemo(() => selectors.makeSelectWebhookById(), []); + + const webhook = useSelector((state) => selectWebhookById(state, id)); + + const dispatch = useDispatch(); + const [t] = useTranslation(); + const [isOpened, toggleOpened] = useToggle(); + + const defaultData = useMemo( + () => ({ + name: webhook.name, + url: webhook.url, + accessToken: webhook.accessToken, + events: webhook.events, + excludedEvents: webhook.excludedEvents, + }), + [webhook], + ); + + const [data, handleFieldChange] = useForm(() => ({ + name: '', + url: '', + ...defaultData, + accessToken: defaultData.accessToken || '', + events: defaultData.events || [], + excludedEvents: defaultData.excludedEvents || [], + })); + + const cleanData = useMemo( + () => ({ + ...data, + name: data.name.trim(), + url: data.url.trim(), + accessToken: data.accessToken.trim() || null, + events: data.events.length === 0 ? null : data.events, + excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents, + }), + [data], + ); + + const editorRef = useRef(null); + + const handleDeleteConfirm = useCallback(() => { + dispatch(entryActions.deleteWebhook(id)); + }, [id, dispatch]); + + const handleSubmit = useCallback(() => { + if (!cleanData.name) { + editorRef.current.selectNameField(); + return; + } + + if (!cleanData.url || !isUrl(cleanData.url)) { + editorRef.current.selectUrlField(); + return; + } + + dispatch(entryActions.updateWebhook(id, cleanData)); + }, [id, dispatch, cleanData]); + + const handleOpenClick = useCallback(() => { + toggleOpened(); + }, [toggleOpened]); + + const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep); + + return ( + <> + + + {defaultData.name} + + +
+ + +
+ + +
+ +
+
+ + ); +}); + +Item.propTypes = { + id: PropTypes.string.isRequired, +}; + +export default Item; diff --git a/client/src/components/webhooks/Webhooks/Item.module.scss b/client/src/components/webhooks/Webhooks/Item.module.scss new file mode 100644 index 00000000..a9e611fb --- /dev/null +++ b/client/src/components/webhooks/Webhooks/Item.module.scss @@ -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 + */ + +:global(#app) { + .controls { + display: flex; + justify-content: space-between; + } + + .deleteButton { + box-shadow: 0 1px 0 #cbcccc; + margin-right: 0; + } + + .title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/client/src/components/webhooks/Webhooks/Webhooks.jsx b/client/src/components/webhooks/Webhooks/Webhooks.jsx new file mode 100644 index 00000000..e953ed5c --- /dev/null +++ b/client/src/components/webhooks/Webhooks/Webhooks.jsx @@ -0,0 +1,96 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import React, { useCallback, useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import { Accordion, Button, Form, Segment } from 'semantic-ui-react'; +import { useDidUpdate, useToggle } from '../../../lib/hooks'; + +import { useForm } from '../../../hooks'; +import { isUrl } from '../../../utils/validator'; +import Item from './Item'; +import Editor from './Editor'; + +const DEFAULT_DATA = { + name: '', + url: '', + accessToken: '', + events: [], + excludedEvents: [], +}; + +const Webhooks = React.memo(({ ids, onCreate }) => { + const [t] = useTranslation(); + + const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA); + const [focusNameFieldState, focusNameField] = useToggle(); + + const editorRef = useRef(null); + + const handleCreateSubmit = useCallback(() => { + const cleanData = { + ...data, + name: data.name.trim(), + url: data.url.trim(), + accessToken: data.accessToken.trim() || null, + events: data.events.length === 0 ? null : data.events, + excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents, + }; + + if (!cleanData.name) { + editorRef.current.selectNameField(); + return; + } + + if (!cleanData.url || !isUrl(cleanData.url)) { + editorRef.current.selectUrlField(); + return; + } + + onCreate(cleanData); + setData(DEFAULT_DATA); + focusNameField(); + }, [onCreate, data, setData, focusNameField]); + + useEffect(() => { + if (editorRef.current) { + editorRef.current.focusNameField(); + } + }, []); + + useDidUpdate(() => { + if (editorRef.current) { + editorRef.current.focusNameField(); + } + }, [focusNameFieldState]); + + return ( + <> + {ids.length > 0 && ( + + {ids.map((id) => ( + + ))} + + )} + {ids.length < 10 && ( + +
+ + + +
+ )} + + ); +}); + +Webhooks.propTypes = { + ids: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types + onCreate: PropTypes.func.isRequired, +}; + +export default Webhooks; diff --git a/client/src/components/webhooks/Webhooks/index.js b/client/src/components/webhooks/Webhooks/index.js new file mode 100644 index 00000000..d64bf58c --- /dev/null +++ b/client/src/components/webhooks/Webhooks/index.js @@ -0,0 +1,8 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import Webhooks from './Webhooks'; + +export default Webhooks; diff --git a/client/src/constants/ActionTypes.js b/client/src/constants/ActionTypes.js index 567afcee..e95736c0 100644 --- a/client/src/constants/ActionTypes.js +++ b/client/src/constants/ActionTypes.js @@ -42,6 +42,21 @@ export default { MODAL_OPEN: 'MODAL_OPEN', MODAL_CLOSE: 'MODAL_CLOSE', + /* Webhooks */ + + WEBHOOK_CREATE: 'WEBHOOK_CREATE', + WEBHOOK_CREATE__SUCCESS: 'WEBHOOK_CREATE__SUCCESS', + WEBHOOK_CREATE__FAILURE: 'WEBHOOK_CREATE__FAILURE', + WEBHOOK_CREATE_HANDLE: 'WEBHOOK_CREATE_HANDLE', + WEBHOOK_UPDATE: 'WEBHOOK_UPDATE', + WEBHOOK_UPDATE__SUCCESS: 'WEBHOOK_UPDATE__SUCCESS', + WEBHOOK_UPDATE__FAILURE: 'WEBHOOK_UPDATE__FAILURE', + WEBHOOK_UPDATE_HANDLE: 'WEBHOOK_UPDATE_HANDLE', + WEBHOOK_DELETE: 'WEBHOOK_DELETE', + WEBHOOK_DELETE__SUCCESS: 'WEBHOOK_DELETE__SUCCESS', + WEBHOOK_DELETE__FAILURE: 'WEBHOOK_DELETE__FAILURE', + WEBHOOK_DELETE_HANDLE: 'WEBHOOK_DELETE_HANDLE', + /* Users */ USER_CREATE: 'USER_CREATE', diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js index 9195eac1..5acdadc8 100755 --- a/client/src/constants/EntryActionTypes.js +++ b/client/src/constants/EntryActionTypes.js @@ -31,6 +31,15 @@ export default { MODAL_OPEN: `${PREFIX}/MODAL_OPEN`, MODAL_CLOSE: `${PREFIX}/MODAL_CLOSE`, + /* Webhooks */ + + WEBHOOK_CREATE: `${PREFIX}/WEBHOOK_CREATE`, + WEBHOOK_CREATE_HANDLE: `${PREFIX}/WEBHOOK_CREATE_HANDLE`, + WEBHOOK_UPDATE: `${PREFIX}/WEBHOOK_UPDATE`, + WEBHOOK_UPDATE_HANDLE: `${PREFIX}/WEBHOOK_UPDATE_HANDLE`, + WEBHOOK_DELETE: `${PREFIX}/WEBHOOK_DELETE`, + WEBHOOK_DELETE_HANDLE: `${PREFIX}/WEBHOOK_DELETE_HANDLE`, + /* Users */ USER_CREATE: `${PREFIX}/USER_CREATE`, diff --git a/client/src/constants/WebhookEvents.js b/client/src/constants/WebhookEvents.js new file mode 100644 index 00000000..886ccfb2 --- /dev/null +++ b/client/src/constants/WebhookEvents.js @@ -0,0 +1,91 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +export default [ + 'actionCreate', + + 'attachmentCreate', + 'attachmentUpdate', + 'attachmentDelete', + + 'backgroundImageCreate', + 'backgroundImageDelete', + + 'baseCustomFieldGroupCreate', + 'baseCustomFieldGroupUpdate', + 'baseCustomFieldGroupDelete', + + 'boardCreate', + 'boardUpdate', + 'boardDelete', + + 'boardMembershipCreate', + 'boardMembershipUpdate', + 'boardMembershipDelete', + + 'cardCreate', + 'cardUpdate', + 'cardDelete', + + 'cardLabelCreate', + 'cardLabelDelete', + + 'cardMembershipCreate', + 'cardMembershipDelete', + + 'commentCreate', + 'commentUpdate', + 'commentDelete', + + 'customFieldCreate', + 'customFieldUpdate', + 'customFieldDelete', + + 'customFieldGroupCreate', + 'customFieldGroupUpdate', + 'customFieldGroupDelete', + + 'customFieldValueUpdate', + 'customFieldValueDelete', + + 'labelCreate', + 'labelUpdate', + 'labelDelete', + + 'listCreate', + 'listUpdate', + 'listClear', + 'listDelete', + + 'notificationCreate', + 'notificationUpdate', + + 'notificationServiceCreate', + 'notificationServiceUpdate', + 'notificationServiceDelete', + + 'projectCreate', + 'projectUpdate', + 'projectDelete', + + 'projectManagerCreate', + 'projectManagerDelete', + + 'taskCreate', + 'taskUpdate', + 'taskDelete', + + 'taskListCreate', + 'taskListUpdate', + 'taskListDelete', + + 'userCreate', + 'userUpdate', + 'userDelete', + + 'webhookCreate', + 'webhookUpdate', + 'webhookDelete', +]; diff --git a/client/src/entry-actions/index.js b/client/src/entry-actions/index.js index 3c9b2f4e..a0356efc 100755 --- a/client/src/entry-actions/index.js +++ b/client/src/entry-actions/index.js @@ -7,6 +7,7 @@ import socket from './socket'; import login from './login'; import core from './core'; import modals from './modals'; +import webhooks from './webhooks'; import users from './users'; import projects from './projects'; import projectManagers from './project-managers'; @@ -33,6 +34,7 @@ export default { ...login, ...core, ...modals, + ...webhooks, ...users, ...projects, ...projectManagers, diff --git a/client/src/entry-actions/webhooks.js b/client/src/entry-actions/webhooks.js new file mode 100644 index 00000000..ff6c7b17 --- /dev/null +++ b/client/src/entry-actions/webhooks.js @@ -0,0 +1,58 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import EntryActionTypes from '../constants/EntryActionTypes'; + +const createWebhook = (data) => ({ + type: EntryActionTypes.WEBHOOK_CREATE, + payload: { + data, + }, +}); + +const handleWebhookCreate = (webhook) => ({ + type: EntryActionTypes.WEBHOOK_CREATE_HANDLE, + payload: { + webhook, + }, +}); + +const updateWebhook = (id, data) => ({ + type: EntryActionTypes.WEBHOOK_UPDATE, + payload: { + id, + data, + }, +}); + +const handleWebhookUpdate = (webhook) => ({ + type: EntryActionTypes.WEBHOOK_UPDATE_HANDLE, + payload: { + webhook, + }, +}); + +const deleteWebhook = (id) => ({ + type: EntryActionTypes.WEBHOOK_DELETE, + payload: { + id, + }, +}); + +const handleWebhookDelete = (webhook) => ({ + type: EntryActionTypes.WEBHOOK_DELETE_HANDLE, + payload: { + webhook, + }, +}); + +export default { + createWebhook, + handleWebhookCreate, + updateWebhook, + handleWebhookUpdate, + deleteWebhook, + handleWebhookDelete, +}; diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js index 01b3255d..ad944cc1 100644 --- a/client/src/locales/en-GB/core.js +++ b/client/src/locales/en-GB/core.js @@ -21,6 +21,7 @@ export default { translation: { common: { aboutPlanka: 'About PLANKA', + accessToken: 'Access token', account: 'Account', actions: 'Actions', activateUser_title: 'Activate User', @@ -69,6 +70,7 @@ export default { areYouSureYouWantToDeleteThisTask: 'Are you sure you want to delete this task?', areYouSureYouWantToDeleteThisTaskList: 'Are you sure you want to delete this task list?', areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?', + areYouSureYouWantToDeleteThisWebhook: 'Are you sure you want to delete this webhook?', areYouSureYouWantToEmptyTrash: 'Are you sure you want to empty the trash?', areYouSureYouWantToLeaveBoard: 'Are you sure you want to leave the board?', areYouSureYouWantToLeaveProject: 'Are you sure you want to leave the project?', @@ -152,6 +154,7 @@ export default { deleteTask_title: 'Delete Task', deleteUser_title: 'Delete User', deletedUser_title: 'Deleted User', + deleteWebhook_title: 'Delete Webhook', description: 'Description', detectAutomatically: 'Detect automatically', display: 'Display', @@ -182,6 +185,8 @@ export default { enterFilename: 'Enter filename', enterListTitle: 'Enter list title...', enterTaskDescription: 'Enter task description...', + events: 'Events', + excludedEvents: 'Excluded events', filterByLabels_title: 'Filter By Labels', filterByMembers_title: 'Filter By Members', forPersonalProjects: 'For personal projects.', @@ -286,6 +291,7 @@ export default { typeTitleToConfirm: 'Type the title to confirm.', unsavedChanges: 'Unsaved changes', uploadedImages: 'Uploaded images', + url: 'URL', userActions_title: 'User Actions', userAddedCardToList: '<0>{{user}} added <2>{{card}} to {{list}}', userAddedThisCardToList: '<0>{{user}} added this card to {{list}}', @@ -313,6 +319,7 @@ export default { userRemovedUserFromThisCard: '<0>{{actorUser}} removed {{removedUser}} from this card', username: 'Username', users: 'Users', + webhooks: 'Webhooks', viewer: 'Viewer', viewers: 'Viewers', visualTaskManagementWithLists: 'Visual task management with lists.', @@ -338,6 +345,7 @@ export default { addTaskList: 'Add task list', addToCard: 'Add to card', addUser: 'Add user', + addWebhook: 'Add webhook', archive: 'Archive', archiveCard: 'Archive card', archiveCard_title: 'Archive Card', @@ -378,6 +386,7 @@ export default { deleteTask_title: 'Delete Task', deleteUser: 'Delete user', deleteUser_title: 'Delete User', + deleteWebhook: 'Delete webhook', dismissAll: 'Dismiss all', duplicate: 'Duplicate', duplicateCard_title: 'Duplicate Card', diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js index f38ae4ed..e29cb19d 100644 --- a/client/src/locales/en-US/core.js +++ b/client/src/locales/en-US/core.js @@ -16,6 +16,7 @@ export default { translation: { common: { aboutPlanka: 'About PLANKA', + accessToken: 'Access token', account: 'Account', actions: 'Actions', activateUser_title: 'Activate User', @@ -64,6 +65,7 @@ export default { areYouSureYouWantToDeleteThisTask: 'Are you sure you want to delete this task?', areYouSureYouWantToDeleteThisTaskList: 'Are you sure you want to delete this task list?', areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?', + areYouSureYouWantToDeleteThisWebhook: 'Are you sure you want to delete this webhook?', areYouSureYouWantToEmptyTrash: 'Are you sure you want to empty the trash?', areYouSureYouWantToLeaveBoard: 'Are you sure you want to leave the board?', areYouSureYouWantToLeaveProject: 'Are you sure you want to leave the project?', @@ -147,6 +149,7 @@ export default { deleteTask_title: 'Delete Task', deleteUser_title: 'Delete User', deletedUser_title: 'Deleted User', + deleteWebhook_title: 'Delete Webhook', description: 'Description', detectAutomatically: 'Detect automatically', display: 'Display', @@ -177,6 +180,8 @@ export default { enterFilename: 'Enter filename', enterListTitle: 'Enter list title...', enterTaskDescription: 'Enter task description...', + events: 'Events', + excludedEvents: 'Excluded events', filterByLabels_title: 'Filter By Labels', filterByMembers_title: 'Filter By Members', forPersonalProjects: 'For personal projects.', @@ -281,6 +286,7 @@ export default { typeTitleToConfirm: 'Type the title to confirm.', unsavedChanges: 'Unsaved changes', uploadedImages: 'Uploaded images', + url: 'URL', userActions_title: 'User Actions', userAddedCardToList: '<0>{{user}} added <2>{{card}} to {{list}}', userAddedThisCardToList: '<0>{{user}} added this card to {{list}}', @@ -308,6 +314,7 @@ export default { userRemovedUserFromThisCard: '<0>{{actorUser}} removed {{removedUser}} from this card', username: 'Username', users: 'Users', + webhooks: 'Webhooks', viewer: 'Viewer', viewers: 'Viewers', visualTaskManagementWithLists: 'Visual task management with lists.', @@ -333,6 +340,7 @@ export default { addTaskList: 'Add task list', addToCard: 'Add to card', addUser: 'Add user', + addWebhook: 'Add webhook', archive: 'Archive', archiveCard: 'Archive card', archiveCard_title: 'Archive Card', @@ -373,6 +381,7 @@ export default { deleteTask_title: 'Delete Task', deleteUser: 'Delete user', deleteUser_title: 'Delete User', + deleteWebhook: 'Delete webhook', dismissAll: 'Dismiss all', duplicate: 'Duplicate', duplicateCard_title: 'Duplicate Card', diff --git a/client/src/models/Webhook.js b/client/src/models/Webhook.js new file mode 100644 index 00000000..b7dfd338 --- /dev/null +++ b/client/src/models/Webhook.js @@ -0,0 +1,88 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import { attr, fk } from 'redux-orm'; + +import BaseModel from './BaseModel'; +import ActionTypes from '../constants/ActionTypes'; + +export default class extends BaseModel { + static modelName = 'Webhook'; + + static fields = { + id: attr(), + name: attr(), + url: attr(), + accessToken: attr(), + events: attr(), + excludedEvents: attr(), + boardId: fk({ + to: 'Board', + as: 'board', + relatedName: 'webhooks', + }), + }; + + static reducer({ type, payload }, Webhook) { + switch (type) { + case ActionTypes.SOCKET_RECONNECT_HANDLE: + Webhook.all().delete(); + + payload.webhooks.forEach((webhook) => { + Webhook.upsert(webhook); + }); + + break; + case ActionTypes.CORE_INITIALIZE: + case ActionTypes.USER_UPDATE_HANDLE: + if (payload.webhooks) { + payload.webhooks.forEach((webhook) => { + Webhook.upsert(webhook); + }); + } + + break; + case ActionTypes.WEBHOOK_CREATE: + case ActionTypes.WEBHOOK_CREATE_HANDLE: + case ActionTypes.WEBHOOK_UPDATE__SUCCESS: + case ActionTypes.WEBHOOK_UPDATE_HANDLE: + Webhook.upsert(payload.webhook); + + break; + case ActionTypes.WEBHOOK_CREATE__SUCCESS: + Webhook.withId(payload.localId).delete(); + Webhook.upsert(payload.webhook); + + break; + case ActionTypes.WEBHOOK_CREATE__FAILURE: + Webhook.withId(payload.localId).delete(); + + break; + case ActionTypes.WEBHOOK_UPDATE: + Webhook.withId(payload.id).update(payload.data); + + break; + case ActionTypes.WEBHOOK_DELETE: + Webhook.withId(payload.id).delete(); + + break; + case ActionTypes.WEBHOOK_DELETE__SUCCESS: + case ActionTypes.WEBHOOK_DELETE_HANDLE: { + const webhookModel = Webhook.withId(payload.webhook.id); + + if (webhookModel) { + webhookModel.delete(); + } + + break; + } + default: + } + } + + static getAllQuerySet() { + return this.orderBy(['id.length', 'id']); + } +} diff --git a/client/src/models/index.js b/client/src/models/index.js index 94616eef..2176310d 100755 --- a/client/src/models/index.js +++ b/client/src/models/index.js @@ -3,6 +3,7 @@ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md */ +import Webhook from './Webhook'; import User from './User'; import Project from './Project'; import ProjectManager from './ProjectManager'; @@ -25,6 +26,7 @@ import Notification from './Notification'; import NotificationService from './NotificationService'; export { + Webhook, User, Project, ProjectManager, diff --git a/client/src/orm.js b/client/src/orm.js index 653f5bb7..f992ace2 100755 --- a/client/src/orm.js +++ b/client/src/orm.js @@ -26,6 +26,7 @@ import { Task, TaskList, User, + Webhook, } from './models'; const orm = new ORM({ @@ -33,6 +34,7 @@ const orm = new ORM({ }); orm.register( + Webhook, User, Project, ProjectManager, diff --git a/client/src/sagas/core/requests/core.js b/client/src/sagas/core/requests/core.js index b3c3778e..0a0c2fa5 100644 --- a/client/src/sagas/core/requests/core.js +++ b/client/src/sagas/core/requests/core.js @@ -10,6 +10,7 @@ import request from '../request'; import api from '../../../api'; import mergeRecords from '../../../utils/merge-records'; import { isUserAdminOrProjectOwner } from '../../../utils/record-helpers'; +import { UserRoles } from '../../../constants/Enums'; export function* fetchCore() { const { @@ -17,6 +18,11 @@ export function* fetchCore() { included: { notificationServices: notificationServices1 }, } = yield call(request, api.getCurrentUser, true); + let webhooks; + if (user.role === UserRoles.ADMIN) { + ({ items: webhooks } = yield call(request, api.getWebhooks)); + } + let users1; if (isUserAdminOrProjectOwner(user)) { ({ items: users1 } = yield call(request, api.getUsers)); @@ -101,6 +107,7 @@ export function* fetchCore() { return { user, board, + webhooks, projectManagers, backgroundImages, baseCustomFieldGroups, diff --git a/client/src/sagas/core/services/core.js b/client/src/sagas/core/services/core.js index 950ca2bd..748d5115 100644 --- a/client/src/sagas/core/services/core.js +++ b/client/src/sagas/core/services/core.js @@ -21,6 +21,7 @@ export function* initializeCore() { const { user, board, + webhooks, users, projects, projectManagers, @@ -50,6 +51,7 @@ export function* initializeCore() { actions.initializeCore( user, board, + webhooks, users, projects, projectManagers, diff --git a/client/src/sagas/core/services/index.js b/client/src/sagas/core/services/index.js index 2e0ab2ff..f371841d 100644 --- a/client/src/sagas/core/services/index.js +++ b/client/src/sagas/core/services/index.js @@ -7,6 +7,7 @@ import router from './router'; import socket from './socket'; import core from './core'; import modals from './modals'; +import webhooks from './webhooks'; import users from './users'; import projects from './projects'; import projectManagers from './project-managers'; @@ -33,6 +34,7 @@ export default { ...socket, ...core, ...modals, + ...webhooks, ...users, ...projects, ...projectManagers, diff --git a/client/src/sagas/core/services/socket.js b/client/src/sagas/core/services/socket.js index 29eff624..a9b6f703 100644 --- a/client/src/sagas/core/services/socket.js +++ b/client/src/sagas/core/services/socket.js @@ -24,6 +24,7 @@ export function* handleSocketReconnect() { let config; let user; let board; + let webhooks; let users; let projects; let projectManagers; @@ -51,6 +52,7 @@ export function* handleSocketReconnect() { ({ user, board, + webhooks, users, projects, projectManagers, @@ -81,6 +83,7 @@ export function* handleSocketReconnect() { config, user, board, + webhooks, users, projects, projectManagers, diff --git a/client/src/sagas/core/services/users.js b/client/src/sagas/core/services/users.js index 5c0495e0..7a7dcacc 100644 --- a/client/src/sagas/core/services/users.js +++ b/client/src/sagas/core/services/users.js @@ -70,6 +70,7 @@ export function* handleUserUpdate(user) { let config; let board; + let webhooks; let users1; let users2; let users3; @@ -102,6 +103,7 @@ export function* handleUserUpdate(user) { if (user.role === UserRoles.ADMIN) { ({ item: config } = yield call(request, api.getConfig)); + ({ items: webhooks } = yield call(request, api.getWebhooks)); ({ items: projects, @@ -164,6 +166,7 @@ export function* handleUserUpdate(user) { boardIds, config, board, + webhooks, mergeRecords(users1, users2, users3), projects, projectManagers, diff --git a/client/src/sagas/core/services/webhooks.js b/client/src/sagas/core/services/webhooks.js new file mode 100644 index 00000000..e55ae399 --- /dev/null +++ b/client/src/sagas/core/services/webhooks.js @@ -0,0 +1,89 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import { call, put } from 'redux-saga/effects'; + +import request from '../request'; +import actions from '../../../actions'; +import api from '../../../api'; +import { createLocalId } from '../../../utils/local-id'; + +export function* createWebhook(data) { + const localId = yield call(createLocalId); + + yield put( + actions.createWebhook({ + ...data, + id: localId, + }), + ); + + let webhook; + try { + ({ item: webhook } = yield call(request, api.createWebhook, { + ...data, + events: data.events && data.events.join(','), + excludedEvents: data.excludedEvents && data.excludedEvents.join(','), + })); + } catch (error) { + yield put(actions.createWebhook.failure(localId, error)); + return; + } + + yield put(actions.createWebhook.success(localId, webhook)); +} + +export function* handleWebhookCreate(webhook) { + yield put(actions.handleWebhookCreate(webhook)); +} + +export function* updateWebhook(id, data) { + yield put(actions.updateWebhook(id, data)); + + let webhook; + try { + ({ item: webhook } = yield call(request, api.updateWebhook, id, { + ...data, + events: data.events && data.events.join(','), + excludedEvents: data.excludedEvents && data.excludedEvents.join(','), + })); + } catch (error) { + yield put(actions.updateWebhook.failure(id, error)); + return; + } + + yield put(actions.updateWebhook.success(webhook)); +} + +export function* handleWebhookUpdate(webhook) { + yield put(actions.handleWebhookUpdate(webhook)); +} + +export function* deleteWebhook(id) { + yield put(actions.deleteWebhook(id)); + + let webhook; + try { + ({ item: webhook } = yield call(request, api.deleteWebhook, id)); + } catch (error) { + yield put(actions.deleteWebhook.failure(id, error)); + return; + } + + yield put(actions.deleteWebhook.success(webhook)); +} + +export function* handleWebhookDelete(webhook) { + yield put(actions.handleWebhookDelete(webhook)); +} + +export default { + createWebhook, + handleWebhookCreate, + updateWebhook, + handleWebhookUpdate, + deleteWebhook, + handleWebhookDelete, +}; diff --git a/client/src/sagas/core/watchers/index.js b/client/src/sagas/core/watchers/index.js index fbe666e0..a6fd8fb3 100755 --- a/client/src/sagas/core/watchers/index.js +++ b/client/src/sagas/core/watchers/index.js @@ -7,6 +7,7 @@ import router from './router'; import socket from './socket'; import core from './core'; import modals from './modals'; +import webhooks from './webhooks'; import users from './users'; import projects from './projects'; import projectManagers from './project-managers'; @@ -33,6 +34,7 @@ export default [ socket, core, modals, + webhooks, users, projects, projectManagers, diff --git a/client/src/sagas/core/watchers/webhooks.js b/client/src/sagas/core/watchers/webhooks.js new file mode 100644 index 00000000..70829be8 --- /dev/null +++ b/client/src/sagas/core/watchers/webhooks.js @@ -0,0 +1,30 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import { all, takeEvery } from 'redux-saga/effects'; + +import services from '../services'; +import EntryActionTypes from '../../../constants/EntryActionTypes'; + +export default function* webhooksWatchers() { + yield all([ + takeEvery(EntryActionTypes.WEBHOOK_CREATE, ({ payload: { data } }) => + services.createWebhook(data), + ), + takeEvery(EntryActionTypes.WEBHOOK_CREATE_HANDLE, ({ payload: { webhook } }) => + services.handleWebhookCreate(webhook), + ), + takeEvery(EntryActionTypes.WEBHOOK_UPDATE, ({ payload: { id, data } }) => + services.updateWebhook(id, data), + ), + takeEvery(EntryActionTypes.WEBHOOK_UPDATE_HANDLE, ({ payload: { webhook } }) => + services.handleWebhookUpdate(webhook), + ), + takeEvery(EntryActionTypes.WEBHOOK_DELETE, ({ payload: { id } }) => services.deleteWebhook(id)), + takeEvery(EntryActionTypes.WEBHOOK_DELETE_HANDLE, ({ payload: { webhook } }) => + services.handleWebhookDelete(webhook), + ), + ]); +} diff --git a/client/src/selectors/index.js b/client/src/selectors/index.js index cccbf87a..82dd0d7a 100755 --- a/client/src/selectors/index.js +++ b/client/src/selectors/index.js @@ -8,6 +8,7 @@ import common from './common'; import core from './core'; import modals from './modals'; import positioning from './positioning'; +import webhooks from './webhooks'; import users from './users'; import projects from './projects'; import projectManagers from './project-managers'; @@ -35,6 +36,7 @@ export default { ...core, ...modals, ...positioning, + ...webhooks, ...users, ...projects, ...projectManagers, diff --git a/client/src/selectors/webhooks.js b/client/src/selectors/webhooks.js new file mode 100644 index 00000000..9d78e3ba --- /dev/null +++ b/client/src/selectors/webhooks.js @@ -0,0 +1,41 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import { createSelector } from 'redux-orm'; + +import orm from '../orm'; +import { isLocalId } from '../utils/local-id'; + +export const makeSelectWebhookById = () => + createSelector( + orm, + (_, id) => id, + ({ Webhook }, id) => { + const webhookModel = Webhook.withId(id); + + if (!webhookModel) { + return webhookModel; + } + + return { + ...webhookModel.ref, + isPersisted: !isLocalId(webhookModel.id), + }; + }, + ); + +export const selectWebhookById = makeSelectWebhookById(); + +export const selectWebhookIds = createSelector(orm, ({ Webhook }) => + Webhook.getAllQuerySet() + .toRefArray() + .map((webhook) => webhook.id), +); + +export default { + makeSelectWebhookById, + selectWebhookById, + selectWebhookIds, +}; diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 0c9bb00d..59ccb824 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -80,15 +80,6 @@ services: # - SMTP_PASSWORD= # - SMTP_FROM="Demo Demo" # - SMTP_TLS_REJECT_UNAUTHORIZED=false - - # Optional fields: accessToken, events, excludedEvents - # - | - # WEBHOOKS=[{ - # "url": "http://localhost:3001", - # "accessToken": "notaccesstoken", - # "events": ["cardCreate", "cardUpdate", "cardDelete"], - # "excludedEvents": ["notificationCreate", "notificationUpdate"] - # }] depends_on: postgres: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index 8bf64544..78ce2ddb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,15 +100,6 @@ services: # - SMTP_PASSWORD__FILE=/run/secrets/smtp_password # - SMTP_FROM="Demo Demo" # - SMTP_TLS_REJECT_UNAUTHORIZED=false - - # Optional fields: accessToken, events, excludedEvents - # - | - # WEBHOOKS=[{ - # "url": "http://localhost:3001", - # "accessToken": "notaccesstoken", - # "events": ["cardCreate", "cardUpdate", "cardDelete"], - # "excludedEvents": ["notificationCreate", "notificationUpdate"] - # }] depends_on: postgres: condition: service_healthy diff --git a/server/.env.sample b/server/.env.sample index 8817bb76..4196895a 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -72,14 +72,6 @@ SECRET_KEY=notsecretkey # SMTP_FROM="Demo Demo" # SMTP_TLS_REJECT_UNAUTHORIZED=false -# Optional fields: accessToken, events, excludedEvents -# WEBHOOKS='[{ -# "url": "http://localhost:3001", -# "accessToken": "notaccesstoken", -# "events": ["cardCreate", "cardUpdate", "cardDelete"], -# "excludedEvents": ["notificationCreate", "notificationUpdate"] -# }]' - ## Do not edit this TZ=UTC diff --git a/server/api/controllers/webhooks/create.js b/server/api/controllers/webhooks/create.js new file mode 100644 index 00000000..9caf21d6 --- /dev/null +++ b/server/api/controllers/webhooks/create.js @@ -0,0 +1,76 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +const { isUrl } = require('../../../utils/validators'); + +const Errors = { + LIMIT_REACHED: { + limitReached: 'Limit reached', + }, +}; + +module.exports = { + inputs: { + name: { + type: 'string', + maxLength: 128, + required: true, + }, + url: { + type: 'string', + maxLength: 2048, + custom: isUrl, + required: true, + }, + accessToken: { + type: 'string', + isNotEmptyString: true, + maxLength: 512, + allowNull: true, + }, + events: { + type: 'string', + isNotEmptyString: true, + maxLength: 2048, + allowNull: true, + }, + excludedEvents: { + type: 'string', + isNotEmptyString: true, + maxLength: 2048, + allowNull: true, + }, + }, + + exits: { + limitReached: { + responseType: 'conflict', + }, + }, + + async fn(inputs) { + const { currentUser } = this.req; + + const values = _.pick(inputs, ['name', 'url', 'accessToken']); + const events = inputs.events && inputs.events.split(','); + const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(','); + + const webhook = await sails.helpers.webhooks.createOne + .with({ + values: { + ...values, + events, + excludedEvents, + }, + actorUser: currentUser, + request: this.req, + }) + .intercept('limitReached', () => Errors.LIMIT_REACHED); + + return { + item: webhook, + }; + }, +}; diff --git a/server/api/controllers/webhooks/delete.js b/server/api/controllers/webhooks/delete.js new file mode 100644 index 00000000..1c23837e --- /dev/null +++ b/server/api/controllers/webhooks/delete.js @@ -0,0 +1,51 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +const { idInput } = require('../../../utils/inputs'); + +const Errors = { + WEBHOOK_NOT_FOUND: { + webhookNotFound: 'Webhook not found', + }, +}; + +module.exports = { + inputs: { + id: { + ...idInput, + required: true, + }, + }, + + exits: { + webhookNotFound: { + responseType: 'notFound', + }, + }, + + async fn(inputs) { + const { currentUser } = this.req; + + let webhook = await Webhook.qm.getOneById(inputs.id); + + if (!webhook) { + throw Errors.WEBHOOK_NOT_FOUND; + } + + webhook = await sails.helpers.webhooks.deleteOne.with({ + record: webhook, + actorUser: currentUser, + request: this.req, + }); + + if (!webhook) { + throw Errors.WEBHOOK_NOT_FOUND; + } + + return { + item: webhook, + }; + }, +}; diff --git a/server/api/controllers/webhooks/index.js b/server/api/controllers/webhooks/index.js new file mode 100644 index 00000000..73aea6fe --- /dev/null +++ b/server/api/controllers/webhooks/index.js @@ -0,0 +1,14 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +module.exports = { + async fn() { + const webhooks = await Webhook.qm.getAll(); + + return { + items: webhooks, + }; + }, +}; diff --git a/server/api/controllers/webhooks/update.js b/server/api/controllers/webhooks/update.js new file mode 100644 index 00000000..8eff2fd3 --- /dev/null +++ b/server/api/controllers/webhooks/update.js @@ -0,0 +1,89 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +const { isUrl } = require('../../../utils/validators'); +const { idInput } = require('../../../utils/inputs'); + +const Errors = { + WEBHOOK_NOT_FOUND: { + webhookNotFound: 'Webhook not found', + }, +}; + +module.exports = { + inputs: { + id: { + ...idInput, + required: true, + }, + name: { + type: 'string', + isNotEmptyString: true, + maxLength: 128, + }, + url: { + type: 'string', + maxLength: 2048, + custom: isUrl, + }, + accessToken: { + type: 'string', + isNotEmptyString: true, + maxLength: 512, + allowNull: true, + }, + events: { + type: 'string', + isNotEmptyString: true, + maxLength: 2048, + allowNull: true, + }, + excludedEvents: { + type: 'string', + isNotEmptyString: true, + maxLength: 2048, + allowNull: true, + }, + }, + + exits: { + webhookNotFound: { + responseType: 'notFound', + }, + }, + + async fn(inputs) { + const { currentUser } = this.req; + + let webhook = await Webhook.qm.getOneById(inputs.id); + + if (!webhook) { + throw Errors.WEBHOOK_NOT_FOUND; + } + + const values = _.pick(inputs, ['name', 'url', 'accessToken']); + const events = inputs.events && inputs.events.split(','); + const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(','); + + webhook = await sails.helpers.webhooks.updateOne.with({ + record: webhook, + values: { + ...values, + events, + excludedEvents, + }, + actorUser: currentUser, + request: this.req, + }); + + if (!webhook) { + throw Errors.WEBHOOK_NOT_FOUND; + } + + return { + item: webhook, + }; + }, +}; diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js index d3d448cb..12bda543 100644 --- a/server/api/helpers/actions/create-one.js +++ b/server/api/helpers/actions/create-one.js @@ -105,6 +105,10 @@ module.exports = { type: 'ref', required: true, }, + webhooks: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -130,7 +134,8 @@ module.exports = { ); sails.helpers.utils.sendWebhooks.with({ - event: 'actionCreate', + webhooks: inputs.webhooks, + event: Webhook.Events.ACTION_CREATE, buildData: () => ({ item: action, included: { @@ -158,6 +163,7 @@ module.exports = { project: inputs.project, board: inputs.board, list: inputs.list, + webhooks: inputs.webhooks, }); } } else { @@ -187,6 +193,7 @@ module.exports = { project: inputs.project, board: inputs.board, list: inputs.list, + webhooks: inputs.webhooks, }), ), ); diff --git a/server/api/helpers/attachments/create-one.js b/server/api/helpers/attachments/create-one.js index 6cc580aa..76a881cd 100644 --- a/server/api/helpers/attachments/create-one.js +++ b/server/api/helpers/attachments/create-one.js @@ -48,8 +48,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'attachmentCreate', + webhooks, + event: Webhook.Events.ATTACHMENT_CREATE, buildData: () => ({ item: sails.helpers.attachments.presentOne(attachment), included: { @@ -65,6 +68,7 @@ module.exports = { if (!values.card.coverAttachmentId) { if (attachment.type === Attachment.Types.FILE && attachment.data.image) { await sails.helpers.cards.updateOne.with({ + webhooks, record: values.card, values: { coverAttachmentId: attachment.id, diff --git a/server/api/helpers/attachments/delete-one.js b/server/api/helpers/attachments/delete-one.js index dcecf346..79195db2 100644 --- a/server/api/helpers/attachments/delete-one.js +++ b/server/api/helpers/attachments/delete-one.js @@ -37,6 +37,7 @@ module.exports = { async fn(inputs) { if (inputs.record.id === inputs.card.coverAttachmentId) { await sails.helpers.cards.updateOne.with({ + webhooks, record: inputs.card, values: { coverAttachmentId: null, @@ -66,8 +67,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'attachmentDelete', + webhooks, + event: Webhook.Events.ATTACHMENT_DELETE, buildData: () => ({ item: sails.helpers.attachments.presentOne(attachment), included: { diff --git a/server/api/helpers/attachments/update-one.js b/server/api/helpers/attachments/update-one.js index 1e34b211..942934ee 100644 --- a/server/api/helpers/attachments/update-one.js +++ b/server/api/helpers/attachments/update-one.js @@ -53,8 +53,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'attachmentUpdate', + webhooks, + event: Webhook.Events.ATTACHMENT_UPDATE, buildData: () => ({ item: sails.helpers.attachments.presentOne(attachment), included: { diff --git a/server/api/helpers/background-images/create-one.js b/server/api/helpers/background-images/create-one.js index 83d80057..4553ceb0 100644 --- a/server/api/helpers/background-images/create-one.js +++ b/server/api/helpers/background-images/create-one.js @@ -47,8 +47,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'backgroundImageCreate', + webhooks, + event: Webhook.Events.BACKGROUND_IMAGE_CREATE, buildData: () => ({ item: sails.helpers.backgroundImages.presentOne(backgroundImage), included: { @@ -60,6 +63,7 @@ module.exports = { await sails.helpers.projects.updateOne.with({ scoper, + webhooks, record: values.project, values: { backgroundImage, diff --git a/server/api/helpers/background-images/delete-one.js b/server/api/helpers/background-images/delete-one.js index 4ae383b1..69908d70 100644 --- a/server/api/helpers/background-images/delete-one.js +++ b/server/api/helpers/background-images/delete-one.js @@ -31,6 +31,7 @@ module.exports = { if (inputs.record.id === inputs.project.backgroundImageId) { await sails.helpers.projects.updateOne.with({ scoper, + webhooks, record: inputs.project, values: { backgroundType: null, @@ -58,8 +59,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'backgroundImageDelete', + webhooks, + event: Webhook.Events.BACKGROUND_IMAGE_DELETE, buildData: () => ({ item: sails.helpers.backgroundImages.presentOne(backgroundImage), included: { diff --git a/server/api/helpers/base-custom-field-groups/create-one.js b/server/api/helpers/base-custom-field-groups/create-one.js index 46fc0a18..6841f9df 100644 --- a/server/api/helpers/base-custom-field-groups/create-one.js +++ b/server/api/helpers/base-custom-field-groups/create-one.js @@ -43,8 +43,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'baseCustomFieldGroupCreate', + webhooks, + event: Webhook.Events.BASE_CUSTOM_FIELD_GROUP_CREATE, buildData: () => ({ item: baseCustomFieldGroup, included: { diff --git a/server/api/helpers/base-custom-field-groups/delete-one.js b/server/api/helpers/base-custom-field-groups/delete-one.js index 7b62e8c4..e23d8378 100644 --- a/server/api/helpers/base-custom-field-groups/delete-one.js +++ b/server/api/helpers/base-custom-field-groups/delete-one.js @@ -45,8 +45,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'baseCustomFieldGroupDelete', + webhooks, + event: Webhook.Events.BASE_CUSTOM_FIELD_GROUP_DELETE, buildData: () => ({ item: baseCustomFieldGroup, included: { diff --git a/server/api/helpers/base-custom-field-groups/update-one.js b/server/api/helpers/base-custom-field-groups/update-one.js index 8a7c040a..2057fedf 100644 --- a/server/api/helpers/base-custom-field-groups/update-one.js +++ b/server/api/helpers/base-custom-field-groups/update-one.js @@ -49,8 +49,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'baseCustomFieldGroupUpdate', + webhooks, + event: Webhook.Events.BASE_CUSTOM_FIELD_GROUP_UPDATE, buildData: () => ({ item: baseCustomFieldGroup, included: { diff --git a/server/api/helpers/board-memberships/create-one.js b/server/api/helpers/board-memberships/create-one.js index 72526c0d..32349194 100644 --- a/server/api/helpers/board-memberships/create-one.js +++ b/server/api/helpers/board-memberships/create-one.js @@ -86,8 +86,11 @@ module.exports = { }); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'boardMembershipCreate', + webhooks, + event: Webhook.Events.BOARD_MEMBERSHIP_CREATE, buildData: () => ({ item: boardMembership, included: { diff --git a/server/api/helpers/board-memberships/delete-one.js b/server/api/helpers/board-memberships/delete-one.js index 9a608a14..29f09adf 100644 --- a/server/api/helpers/board-memberships/delete-one.js +++ b/server/api/helpers/board-memberships/delete-one.js @@ -106,8 +106,11 @@ module.exports = { }); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'boardMembershipDelete', + webhooks, + event: Webhook.Events.BOARD_MEMBERSHIP_DELETE, buildData: () => ({ item: boardMembership, included: { diff --git a/server/api/helpers/board-memberships/update-one.js b/server/api/helpers/board-memberships/update-one.js index 2ae3382b..880667dd 100644 --- a/server/api/helpers/board-memberships/update-one.js +++ b/server/api/helpers/board-memberships/update-one.js @@ -75,8 +75,11 @@ module.exports = { }); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'boardMembershipUpdate', + webhooks, + event: Webhook.Events.BOARD_MEMBERSHIP_UPDATE, buildData: () => ({ item: boardMembership, included: { diff --git a/server/api/helpers/boards/create-one.js b/server/api/helpers/boards/create-one.js index fb128250..a09a7252 100644 --- a/server/api/helpers/boards/create-one.js +++ b/server/api/helpers/boards/create-one.js @@ -108,8 +108,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'boardCreate', + webhooks, + event: Webhook.Events.BOARD_CREATE, buildData: () => ({ item: board, included: { diff --git a/server/api/helpers/boards/delete-one.js b/server/api/helpers/boards/delete-one.js index fd312e8b..78b49f6b 100644 --- a/server/api/helpers/boards/delete-one.js +++ b/server/api/helpers/boards/delete-one.js @@ -49,8 +49,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'boardDelete', + webhooks, + event: Webhook.Events.BOARD_DELETE, buildData: () => ({ item: board, included: { diff --git a/server/api/helpers/boards/update-one.js b/server/api/helpers/boards/update-one.js index a565687b..858a4a15 100644 --- a/server/api/helpers/boards/update-one.js +++ b/server/api/helpers/boards/update-one.js @@ -104,8 +104,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'boardUpdate', + webhooks, + event: Webhook.Events.BOARD_UPDATE, buildData: () => ({ item: board, included: { diff --git a/server/api/helpers/card-labels/create-one.js b/server/api/helpers/card-labels/create-one.js index 3b57f295..23797301 100644 --- a/server/api/helpers/card-labels/create-one.js +++ b/server/api/helpers/card-labels/create-one.js @@ -61,8 +61,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'cardLabelCreate', + webhooks, + event: Webhook.Events.CARD_LABEL_CREATE, buildData: () => ({ item: cardLabel, included: { diff --git a/server/api/helpers/card-labels/delete-one.js b/server/api/helpers/card-labels/delete-one.js index dffc05fc..7c037fed 100644 --- a/server/api/helpers/card-labels/delete-one.js +++ b/server/api/helpers/card-labels/delete-one.js @@ -47,8 +47,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'cardLabelDelete', + webhooks, + event: Webhook.Events.CARD_LABEL_DELETE, buildData: () => ({ item: cardLabel, included: { diff --git a/server/api/helpers/card-memberships/create-one.js b/server/api/helpers/card-memberships/create-one.js index e71c2e0d..9daf61ba 100644 --- a/server/api/helpers/card-memberships/create-one.js +++ b/server/api/helpers/card-memberships/create-one.js @@ -61,8 +61,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'cardMembershipCreate', + webhooks, + event: Webhook.Events.CARD_MEMBERSHIP_CREATE, buildData: () => ({ item: cardMembership, included: { @@ -106,6 +109,7 @@ module.exports = { } await sails.helpers.actions.createOne.with({ + webhooks, values: { type: Action.Types.ADD_MEMBER_TO_CARD, data: { diff --git a/server/api/helpers/card-memberships/delete-one.js b/server/api/helpers/card-memberships/delete-one.js index 4d07111e..85bed025 100644 --- a/server/api/helpers/card-memberships/delete-one.js +++ b/server/api/helpers/card-memberships/delete-one.js @@ -51,8 +51,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'cardMembershipDelete', + webhooks, + event: Webhook.Events.CARD_MEMBERSHIP_DELETE, buildData: () => ({ item: cardMembership, included: { @@ -82,6 +85,7 @@ module.exports = { } await sails.helpers.actions.createOne.with({ + webhooks, values: { type: Action.Types.REMOVE_MEMBER_FROM_CARD, data: { diff --git a/server/api/helpers/cards/create-one.js b/server/api/helpers/cards/create-one.js index ae05a43e..9040774a 100644 --- a/server/api/helpers/cards/create-one.js +++ b/server/api/helpers/cards/create-one.js @@ -84,8 +84,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'cardCreate', + webhooks, + event: Webhook.Events.CARD_CREATE, buildData: () => ({ item: card, included: { @@ -120,6 +123,7 @@ module.exports = { } await sails.helpers.actions.createOne.with({ + webhooks, values: { card, type: Action.Types.CREATE_CARD, diff --git a/server/api/helpers/cards/delete-one.js b/server/api/helpers/cards/delete-one.js index c357e1aa..5b5ca64e 100644 --- a/server/api/helpers/cards/delete-one.js +++ b/server/api/helpers/cards/delete-one.js @@ -45,8 +45,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'cardDelete', + webhooks, + event: Webhook.Events.CARD_DELETE, buildData: () => ({ item: card, included: { diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js index 3c0f3970..3c81b0dd 100644 --- a/server/api/helpers/cards/duplicate-one.js +++ b/server/api/helpers/cards/duplicate-one.js @@ -228,8 +228,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'cardCreate', + webhooks, + event: Webhook.Events.CARD_CREATE, buildData: () => ({ item: card, included: { @@ -272,6 +275,7 @@ module.exports = { } await sails.helpers.actions.createOne.with({ + webhooks, values: { card, type: Action.Types.CREATE_CARD, // TODO: introduce separate type? diff --git a/server/api/helpers/cards/read-notifications-for-user.js b/server/api/helpers/cards/read-notifications-for-user.js index e6e63ad0..c205e636 100644 --- a/server/api/helpers/cards/read-notifications-for-user.js +++ b/server/api/helpers/cards/read-notifications-for-user.js @@ -31,6 +31,8 @@ module.exports = { }, ); + const webhooks = await Webhook.qm.getAll(); + notifications.forEach((notification) => { sails.sockets.broadcast( `user:${notification.userId}`, @@ -43,7 +45,8 @@ module.exports = { // TODO: with prevData? sails.helpers.utils.sendWebhooks.with({ - event: 'notificationUpdate', + webhooks, + event: Webhook.Events.NOTIFICATION_UPDATE, buildData: () => ({ item: notification, }), diff --git a/server/api/helpers/cards/update-one.js b/server/api/helpers/cards/update-one.js index 26071782..049e4512 100644 --- a/server/api/helpers/cards/update-one.js +++ b/server/api/helpers/cards/update-one.js @@ -31,6 +31,9 @@ module.exports = { type: 'ref', required: true, }, + webhooks: { + type: 'ref', + }, request: { type: 'ref', }, @@ -104,6 +107,8 @@ module.exports = { if (_.isEmpty(values)) { card = inputs.record; } else { + const { webhooks = await Webhook.qm.getAll() } = inputs; + if (!_.isNil(values.position)) { const cards = await Card.qm.getByListId(list.id, { exceptIdOrIds: inputs.record.id, @@ -402,6 +407,7 @@ module.exports = { const { id } = await sails.helpers.labels.createOne.with({ project, + webhooks, values: { ..._.omit(label, ['id', 'boardId', 'createdAt', 'updatedAt']), board, @@ -459,6 +465,7 @@ module.exports = { if (values.list) { await sails.helpers.actions.createOne.with({ + webhooks, values: { card, type: Action.Types.MOVE_CARD, @@ -477,7 +484,8 @@ module.exports = { } sails.helpers.utils.sendWebhooks.with({ - event: 'cardUpdate', + webhooks, + event: Webhook.Events.CARD_UPDATE, buildData: () => ({ item: card, included: { diff --git a/server/api/helpers/comments/create-one.js b/server/api/helpers/comments/create-one.js index eb0936c1..6095e673 100644 --- a/server/api/helpers/comments/create-one.js +++ b/server/api/helpers/comments/create-one.js @@ -79,8 +79,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'commentCreate', + webhooks, + event: Webhook.Events.COMMENT_CREATE, buildData: () => ({ item: comment, included: { @@ -125,6 +128,7 @@ module.exports = { await Promise.all( notifiableUserIds.map((userId) => sails.helpers.notifications.createOne.with({ + webhooks, values: { userId, comment, diff --git a/server/api/helpers/comments/delete-one.js b/server/api/helpers/comments/delete-one.js index 60e362a2..2ce82c50 100644 --- a/server/api/helpers/comments/delete-one.js +++ b/server/api/helpers/comments/delete-one.js @@ -47,8 +47,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'commentDelete', + webhooks, + event: Webhook.Events.COMMENT_DELETE, buildData: () => ({ item: comment, included: { diff --git a/server/api/helpers/comments/update-one.js b/server/api/helpers/comments/update-one.js index f16cb481..2f4eba6a 100644 --- a/server/api/helpers/comments/update-one.js +++ b/server/api/helpers/comments/update-one.js @@ -53,8 +53,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'commentUpdate', + webhooks, + event: Webhook.Events.COMMENT_UPDATE, buildData: () => ({ item: comment, included: { diff --git a/server/api/helpers/custom-field-groups/create-one-in-board.js b/server/api/helpers/custom-field-groups/create-one-in-board.js index e9a2380c..5d90ea3a 100644 --- a/server/api/helpers/custom-field-groups/create-one-in-board.js +++ b/server/api/helpers/custom-field-groups/create-one-in-board.js @@ -87,8 +87,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldGroupCreate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_GROUP_CREATE, buildData: () => ({ item: customFieldGroup, included: { diff --git a/server/api/helpers/custom-field-groups/create-one-in-card.js b/server/api/helpers/custom-field-groups/create-one-in-card.js index bfa510ec..addd15e3 100644 --- a/server/api/helpers/custom-field-groups/create-one-in-card.js +++ b/server/api/helpers/custom-field-groups/create-one-in-card.js @@ -95,8 +95,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldGroupCreate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_GROUP_CREATE, buildData: () => ({ item: customFieldGroup, included: { diff --git a/server/api/helpers/custom-field-groups/delete-one-in-board.js b/server/api/helpers/custom-field-groups/delete-one-in-board.js index e565fa00..92962fbd 100644 --- a/server/api/helpers/custom-field-groups/delete-one-in-board.js +++ b/server/api/helpers/custom-field-groups/delete-one-in-board.js @@ -41,8 +41,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldGroupDelete', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_GROUP_DELETE, buildData: () => ({ item: customFieldGroup, included: { diff --git a/server/api/helpers/custom-field-groups/delete-one-in-card.js b/server/api/helpers/custom-field-groups/delete-one-in-card.js index 045aa06f..40c1c146 100644 --- a/server/api/helpers/custom-field-groups/delete-one-in-card.js +++ b/server/api/helpers/custom-field-groups/delete-one-in-card.js @@ -49,8 +49,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldGroupDelete', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_GROUP_DELETE, buildData: () => ({ item: customFieldGroup, included: { diff --git a/server/api/helpers/custom-field-groups/update-one-in-board.js b/server/api/helpers/custom-field-groups/update-one-in-board.js index 52ff8b5d..96af3e89 100644 --- a/server/api/helpers/custom-field-groups/update-one-in-board.js +++ b/server/api/helpers/custom-field-groups/update-one-in-board.js @@ -91,8 +91,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldGroupUpdate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_GROUP_UPDATE, buildData: () => ({ item: customFieldGroup, included: { diff --git a/server/api/helpers/custom-field-groups/update-one-in-card.js b/server/api/helpers/custom-field-groups/update-one-in-card.js index 2468f5a9..c7928fb0 100644 --- a/server/api/helpers/custom-field-groups/update-one-in-card.js +++ b/server/api/helpers/custom-field-groups/update-one-in-card.js @@ -99,8 +99,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldGroupUpdate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_GROUP_UPDATE, buildData: () => ({ item: customFieldGroup, included: { diff --git a/server/api/helpers/custom-field-values/create-or-update-one.js b/server/api/helpers/custom-field-values/create-or-update-one.js index e69fcdc9..48179575 100644 --- a/server/api/helpers/custom-field-values/create-or-update-one.js +++ b/server/api/helpers/custom-field-values/create-or-update-one.js @@ -49,9 +49,12 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + // TODO: with prevData? sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldValueUpdate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_VALUE_UPDATE, buildData: () => ({ item: customFieldValue, included: { diff --git a/server/api/helpers/custom-field-values/delete-one.js b/server/api/helpers/custom-field-values/delete-one.js index 2ba994de..529683b9 100644 --- a/server/api/helpers/custom-field-values/delete-one.js +++ b/server/api/helpers/custom-field-values/delete-one.js @@ -51,8 +51,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldValueDelete', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_VALUE_DELETE, buildData: () => ({ item: customFieldValue, included: { diff --git a/server/api/helpers/custom-fields/create-one-in-base-custom-field-group.js b/server/api/helpers/custom-fields/create-one-in-base-custom-field-group.js index abfc3ee4..7628e6b9 100644 --- a/server/api/helpers/custom-fields/create-one-in-base-custom-field-group.js +++ b/server/api/helpers/custom-fields/create-one-in-base-custom-field-group.js @@ -93,8 +93,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldCreate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_CREATE, buildData: () => ({ item: customField, included: { diff --git a/server/api/helpers/custom-fields/create-one-in-custom-field-group.js b/server/api/helpers/custom-fields/create-one-in-custom-field-group.js index 61b5a05d..0ae414b8 100644 --- a/server/api/helpers/custom-fields/create-one-in-custom-field-group.js +++ b/server/api/helpers/custom-fields/create-one-in-custom-field-group.js @@ -103,8 +103,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldCreate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_CREATE, buildData: () => ({ item: customField, included: { diff --git a/server/api/helpers/custom-fields/delete-one-in-base-custom-field-group.js b/server/api/helpers/custom-fields/delete-one-in-base-custom-field-group.js index 30a221c9..dd18dd46 100644 --- a/server/api/helpers/custom-fields/delete-one-in-base-custom-field-group.js +++ b/server/api/helpers/custom-fields/delete-one-in-base-custom-field-group.js @@ -49,8 +49,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldDelete', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_DELETE, buildData: () => ({ item: customField, included: { diff --git a/server/api/helpers/custom-fields/delete-one-in-custom-field-group.js b/server/api/helpers/custom-fields/delete-one-in-custom-field-group.js index b3b3dbc7..fa1498df 100644 --- a/server/api/helpers/custom-fields/delete-one-in-custom-field-group.js +++ b/server/api/helpers/custom-fields/delete-one-in-custom-field-group.js @@ -66,8 +66,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldDelete', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_DELETE, buildData: () => ({ item: customField, included: { diff --git a/server/api/helpers/custom-fields/update-one-in-base-custom-field-group.js b/server/api/helpers/custom-fields/update-one-in-base-custom-field-group.js index 887b9a57..73b57c60 100644 --- a/server/api/helpers/custom-fields/update-one-in-base-custom-field-group.js +++ b/server/api/helpers/custom-fields/update-one-in-base-custom-field-group.js @@ -94,8 +94,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldUpdate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_UPDATE, buildData: () => ({ item: customField, included: { diff --git a/server/api/helpers/custom-fields/update-one-in-custom-field-group.js b/server/api/helpers/custom-fields/update-one-in-custom-field-group.js index 7b38ba9e..73953661 100644 --- a/server/api/helpers/custom-fields/update-one-in-custom-field-group.js +++ b/server/api/helpers/custom-fields/update-one-in-custom-field-group.js @@ -109,8 +109,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'customFieldUpdate', + webhooks, + event: Webhook.Events.CUSTOM_FIELD_UPDATE, buildData: () => ({ item: customField, included: { diff --git a/server/api/helpers/labels/create-one.js b/server/api/helpers/labels/create-one.js index 2371f478..98b256b3 100644 --- a/server/api/helpers/labels/create-one.js +++ b/server/api/helpers/labels/create-one.js @@ -17,6 +17,9 @@ module.exports = { type: 'ref', required: true, }, + webhooks: { + type: 'ref', + }, request: { type: 'ref', }, @@ -70,8 +73,11 @@ module.exports = { inputs.request, ); + const { webhooks = await Webhook.qm.getAll() } = inputs; + sails.helpers.utils.sendWebhooks.with({ - event: 'labelCreate', + webhooks, + event: Webhook.Events.LABEL_CREATE, buildData: () => ({ item: label, included: { diff --git a/server/api/helpers/labels/delete-one.js b/server/api/helpers/labels/delete-one.js index 7607e6a4..d770ab4f 100644 --- a/server/api/helpers/labels/delete-one.js +++ b/server/api/helpers/labels/delete-one.js @@ -41,8 +41,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'labelDelete', + webhooks, + event: Webhook.Events.LABEL_DELETE, buildData: () => ({ item: label, included: { diff --git a/server/api/helpers/labels/update-one.js b/server/api/helpers/labels/update-one.js index 6408952f..c83451cb 100644 --- a/server/api/helpers/labels/update-one.js +++ b/server/api/helpers/labels/update-one.js @@ -81,8 +81,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'labelUpdate', + webhooks, + event: Webhook.Events.LABEL_UPDATE, buildData: () => ({ item: label, included: { diff --git a/server/api/helpers/lists/clear-one.js b/server/api/helpers/lists/clear-one.js index 32c594ca..fde825dd 100644 --- a/server/api/helpers/lists/clear-one.js +++ b/server/api/helpers/lists/clear-one.js @@ -38,8 +38,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'listClear', + webhooks, + event: Webhook.Events.LIST_CLEAR, buildData: () => ({ item: inputs.record, included: { diff --git a/server/api/helpers/lists/create-one.js b/server/api/helpers/lists/create-one.js index dc02dff4..04913022 100644 --- a/server/api/helpers/lists/create-one.js +++ b/server/api/helpers/lists/create-one.js @@ -72,8 +72,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'listCreate', + webhooks, + event: Webhook.Events.LIST_CREATE, buildData: () => ({ item: list, included: { diff --git a/server/api/helpers/lists/delete-one.js b/server/api/helpers/lists/delete-one.js index 2f8dd648..7b2eee49 100644 --- a/server/api/helpers/lists/delete-one.js +++ b/server/api/helpers/lists/delete-one.js @@ -57,8 +57,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'listDelete', + webhooks, + event: Webhook.Events.LIST_DELETE, buildData: () => ({ item: list, included: { diff --git a/server/api/helpers/lists/move-cards.js b/server/api/helpers/lists/move-cards.js index a7f8e4b5..57437770 100644 --- a/server/api/helpers/lists/move-cards.js +++ b/server/api/helpers/lists/move-cards.js @@ -91,10 +91,13 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + cards.forEach((card) => { // TODO: with prevData? sails.helpers.utils.sendWebhooks.with({ - event: 'cardUpdate', + webhooks, + event: Webhook.Events.CARD_UPDATE, buildData: () => ({ item: card, included: { diff --git a/server/api/helpers/lists/sort-one.js b/server/api/helpers/lists/sort-one.js index 0daaddbf..5da8e837 100644 --- a/server/api/helpers/lists/sort-one.js +++ b/server/api/helpers/lists/sort-one.js @@ -100,10 +100,13 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + cards.forEach((card) => { // TODO: with prevData? sails.helpers.utils.sendWebhooks.with({ - event: 'cardUpdate', + webhooks, + event: Webhook.Events.CARD_UPDATE, buildData: () => ({ item: card, included: { diff --git a/server/api/helpers/lists/update-one.js b/server/api/helpers/lists/update-one.js index 242bc3be..72a67eb3 100644 --- a/server/api/helpers/lists/update-one.js +++ b/server/api/helpers/lists/update-one.js @@ -84,8 +84,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'listUpdate', + webhooks, + event: Webhook.Events.LIST_UPDATE, buildData: () => ({ item: list, included: { diff --git a/server/api/helpers/notification-services/create-one-in-board.js b/server/api/helpers/notification-services/create-one-in-board.js index e9769f7d..b81282f1 100644 --- a/server/api/helpers/notification-services/create-one-in-board.js +++ b/server/api/helpers/notification-services/create-one-in-board.js @@ -62,8 +62,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'notificationServiceCreate', + webhooks, + event: Webhook.Events.NOTIFICATION_SERVICE_CREATE, buildData: () => ({ item: notificationService, included: { diff --git a/server/api/helpers/notification-services/create-one-in-user.js b/server/api/helpers/notification-services/create-one-in-user.js index 1fe7d187..8030fda5 100644 --- a/server/api/helpers/notification-services/create-one-in-user.js +++ b/server/api/helpers/notification-services/create-one-in-user.js @@ -48,8 +48,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'notificationServiceCreate', + webhooks, + event: Webhook.Events.NOTIFICATION_SERVICE_CREATE, buildData: () => ({ item: notificationService, included: { diff --git a/server/api/helpers/notification-services/delete-one-in-board.js b/server/api/helpers/notification-services/delete-one-in-board.js index 455da16d..7183edbc 100644 --- a/server/api/helpers/notification-services/delete-one-in-board.js +++ b/server/api/helpers/notification-services/delete-one-in-board.js @@ -49,8 +49,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'notificationServiceDelete', + webhooks, + event: Webhook.Events.NOTIFICATION_SERVICE_DELETE, buildData: () => ({ item: notificationService, included: { diff --git a/server/api/helpers/notification-services/delete-one-in-user.js b/server/api/helpers/notification-services/delete-one-in-user.js index 26ec3d41..d71956b0 100644 --- a/server/api/helpers/notification-services/delete-one-in-user.js +++ b/server/api/helpers/notification-services/delete-one-in-user.js @@ -35,8 +35,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'notificationServiceDelete', + webhooks, + event: Webhook.Events.NOTIFICATION_SERVICE_DELETE, buildData: () => ({ item: notificationService, included: { diff --git a/server/api/helpers/notification-services/update-one-in-board.js b/server/api/helpers/notification-services/update-one-in-board.js index cb291bc9..de79c3ff 100644 --- a/server/api/helpers/notification-services/update-one-in-board.js +++ b/server/api/helpers/notification-services/update-one-in-board.js @@ -55,8 +55,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'notificationServiceUpdate', + webhooks, + event: Webhook.Events.NOTIFICATION_SERVICE_UPDATE, buildData: () => ({ item: notificationService, included: { diff --git a/server/api/helpers/notification-services/update-one-in-user.js b/server/api/helpers/notification-services/update-one-in-user.js index d1cd7d5c..f7ee676e 100644 --- a/server/api/helpers/notification-services/update-one-in-user.js +++ b/server/api/helpers/notification-services/update-one-in-user.js @@ -41,8 +41,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'notificationServiceUpdate', + webhooks, + event: Webhook.Events.NOTIFICATION_SERVICE_UPDATE, buildData: () => ({ item: notificationService, included: { diff --git a/server/api/helpers/notifications/create-one.js b/server/api/helpers/notifications/create-one.js index 3b26a6cb..04d2f873 100644 --- a/server/api/helpers/notifications/create-one.js +++ b/server/api/helpers/notifications/create-one.js @@ -214,6 +214,10 @@ module.exports = { type: 'ref', required: true, }, + webhooks: { + type: 'ref', + required: true, + }, }, async fn(inputs) { @@ -248,7 +252,8 @@ module.exports = { }); sails.helpers.utils.sendWebhooks.with({ - event: 'notificationCreate', + webhooks: inputs.webhooks, + event: Webhook.Events.NOTIFICATION_CREATE, buildData: () => ({ item: notification, included: { diff --git a/server/api/helpers/notifications/read-all-for-user.js b/server/api/helpers/notifications/read-all-for-user.js index 240cc785..538c7b65 100644 --- a/server/api/helpers/notifications/read-all-for-user.js +++ b/server/api/helpers/notifications/read-all-for-user.js @@ -26,6 +26,8 @@ module.exports = { }, ); + const webhooks = await Webhook.qm.getAll(); + notifications.forEach((notification) => { sails.sockets.broadcast( `user:${notification.userId}`, @@ -38,7 +40,8 @@ module.exports = { // TODO: with prevData? sails.helpers.utils.sendWebhooks.with({ - event: 'notificationUpdate', + webhooks, + event: Webhook.Events.NOTIFICATION_UPDATE, buildData: () => ({ item: notification, }), diff --git a/server/api/helpers/notifications/update-one.js b/server/api/helpers/notifications/update-one.js index 49675e8f..dcad5f53 100644 --- a/server/api/helpers/notifications/update-one.js +++ b/server/api/helpers/notifications/update-one.js @@ -37,8 +37,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'notificationUpdate', + webhooks, + event: Webhook.Events.NOTIFICATION_UPDATE, buildData: () => ({ item: notification, }), diff --git a/server/api/helpers/project-managers/create-one.js b/server/api/helpers/project-managers/create-one.js index a9ce9bb5..5ef198cf 100644 --- a/server/api/helpers/project-managers/create-one.js +++ b/server/api/helpers/project-managers/create-one.js @@ -13,6 +13,9 @@ module.exports = { type: 'ref', required: true, }, + webhooks: { + type: 'ref', + }, request: { type: 'ref', }, @@ -69,8 +72,11 @@ module.exports = { ); }); + const { webhooks = await Webhook.qm.getAll() } = inputs; + sails.helpers.utils.sendWebhooks.with({ - event: 'projectManagerCreate', + webhooks, + event: Webhook.Events.PROJECT_MANAGER_CREATE, buildData: () => ({ item: projectManager, included: { diff --git a/server/api/helpers/project-managers/delete-one.js b/server/api/helpers/project-managers/delete-one.js index 2c93bed0..fe50f635 100644 --- a/server/api/helpers/project-managers/delete-one.js +++ b/server/api/helpers/project-managers/delete-one.js @@ -85,8 +85,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'projectManagerDelete', + webhooks, + event: Webhook.Events.PROJECT_MANAGER_DELETE, buildData: () => ({ item: projectManager, included: { diff --git a/server/api/helpers/projects/create-one.js b/server/api/helpers/projects/create-one.js index 37de4188..513f305c 100644 --- a/server/api/helpers/projects/create-one.js +++ b/server/api/helpers/projects/create-one.js @@ -44,8 +44,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'projectCreate', + webhooks, + event: Webhook.Events.PROJECT_CREATE, buildData: () => ({ item: project, }), diff --git a/server/api/helpers/projects/delete-one.js b/server/api/helpers/projects/delete-one.js index 3d87fa08..7b11936d 100644 --- a/server/api/helpers/projects/delete-one.js +++ b/server/api/helpers/projects/delete-one.js @@ -51,8 +51,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'projectDelete', + webhooks, + event: Webhook.Events.PROJECT_DELETE, buildData: () => ({ item: project, }), diff --git a/server/api/helpers/projects/update-one.js b/server/api/helpers/projects/update-one.js index bf9621f0..3a4cb674 100644 --- a/server/api/helpers/projects/update-one.js +++ b/server/api/helpers/projects/update-one.js @@ -20,6 +20,9 @@ module.exports = { scoper: { type: 'ref', }, + webhooks: { + type: 'ref', + }, request: { type: 'ref', }, @@ -172,8 +175,11 @@ module.exports = { ); }); + const { webhooks = await Webhook.qm.getAll() } = inputs; + sails.helpers.utils.sendWebhooks.with({ - event: 'projectUpdate', + webhooks, + event: Webhook.Events.PROJECT_UPDATE, buildData: () => ({ item: project, }), diff --git a/server/api/helpers/task-lists/create-one.js b/server/api/helpers/task-lists/create-one.js index ebda0158..e0b0a294 100644 --- a/server/api/helpers/task-lists/create-one.js +++ b/server/api/helpers/task-lists/create-one.js @@ -80,8 +80,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'taskListCreate', + webhooks, + event: Webhook.Events.TASK_LIST_CREATE, buildData: () => ({ item: taskList, included: { diff --git a/server/api/helpers/task-lists/delete-one.js b/server/api/helpers/task-lists/delete-one.js index f607e88f..b88c4684 100644 --- a/server/api/helpers/task-lists/delete-one.js +++ b/server/api/helpers/task-lists/delete-one.js @@ -49,8 +49,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'taskListDelete', + webhooks, + event: Webhook.Events.TASK_LIST_DELETE, buildData: () => ({ item: taskList, included: { diff --git a/server/api/helpers/task-lists/update-one.js b/server/api/helpers/task-lists/update-one.js index d4734d4f..2acc37f2 100644 --- a/server/api/helpers/task-lists/update-one.js +++ b/server/api/helpers/task-lists/update-one.js @@ -91,8 +91,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'taskListUpdate', + webhooks, + event: Webhook.Events.TASK_LIST_UPDATE, buildData: () => ({ item: taskList, included: { diff --git a/server/api/helpers/tasks/create-one.js b/server/api/helpers/tasks/create-one.js index 2bf547aa..6b92a659 100644 --- a/server/api/helpers/tasks/create-one.js +++ b/server/api/helpers/tasks/create-one.js @@ -82,8 +82,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'taskCreate', + webhooks, + event: Webhook.Events.TASK_CREATE, buildData: () => ({ item: task, included: { diff --git a/server/api/helpers/tasks/delete-one.js b/server/api/helpers/tasks/delete-one.js index 9addc940..ab1bdb92 100644 --- a/server/api/helpers/tasks/delete-one.js +++ b/server/api/helpers/tasks/delete-one.js @@ -51,8 +51,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'taskDelete', + webhooks, + event: Webhook.Events.TASK_DELETE, buildData: () => ({ item: task, included: { diff --git a/server/api/helpers/tasks/update-one.js b/server/api/helpers/tasks/update-one.js index c503e915..00ae6a3d 100644 --- a/server/api/helpers/tasks/update-one.js +++ b/server/api/helpers/tasks/update-one.js @@ -112,8 +112,11 @@ module.exports = { inputs.request, ); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'taskUpdate', + webhooks, + event: Webhook.Events.TASK_UPDATE, buildData: () => ({ item: task, included: { @@ -135,6 +138,7 @@ module.exports = { if (inputs.record.isCompleted !== task.isCompleted) { await sails.helpers.actions.createOne.with({ + webhooks, values: { type: task.isCompleted ? Action.Types.COMPLETE_TASK : Action.Types.UNCOMPLETE_TASK, data: { diff --git a/server/api/helpers/users/create-one.js b/server/api/helpers/users/create-one.js index 1681c861..2fca7f5c 100644 --- a/server/api/helpers/users/create-one.js +++ b/server/api/helpers/users/create-one.js @@ -96,8 +96,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'userCreate', + webhooks, + event: Webhook.Events.USER_CREATE, buildData: () => ({ item: sails.helpers.users.presentOne(user), }), diff --git a/server/api/helpers/users/delete-one.js b/server/api/helpers/users/delete-one.js index 705a840b..9e891072 100644 --- a/server/api/helpers/users/delete-one.js +++ b/server/api/helpers/users/delete-one.js @@ -64,8 +64,11 @@ module.exports = { ); }); + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'userDelete', + webhooks, + event: Webhook.Events.USER_DELETE, buildData: () => ({ item: sails.helpers.users.presentOne(user), }), @@ -81,6 +84,7 @@ module.exports = { lonelyProjects.map((project) => // TODO: optimize with scoper sails.helpers.projectManagers.createOne.with({ + webhooks, values: { project, user: inputs.actorUser, diff --git a/server/api/helpers/users/update-one.js b/server/api/helpers/users/update-one.js index 23271bbb..c4098065 100644 --- a/server/api/helpers/users/update-one.js +++ b/server/api/helpers/users/update-one.js @@ -200,8 +200,11 @@ module.exports = { } } + const webhooks = await Webhook.qm.getAll(); + sails.helpers.utils.sendWebhooks.with({ - event: 'userUpdate', + webhooks, + event: Webhook.Events.USER_UPDATE, buildData: () => ({ item: sails.helpers.users.presentOne(user), }), diff --git a/server/api/helpers/utils/send-webhooks.js b/server/api/helpers/utils/send-webhooks.js index b74a4b0f..d992b401 100644 --- a/server/api/helpers/utils/send-webhooks.js +++ b/server/api/helpers/utils/send-webhooks.js @@ -3,88 +3,7 @@ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md */ -const EVENT_TYPES = { - ACTION_CREATE: 'actionCreate', - - ATTACHMENT_CREATE: 'attachmentCreate', - ATTACHMENT_UPDATE: 'attachmentUpdate', - ATTACHMENT_DELETE: 'attachmentDelete', - - BACKGROUND_IMAGE_CREATE: 'backgroundImageCreate', - BACKGROUND_IMAGE_DELETE: 'backgroundImageDelete', - - BASE_CUSTOM_FIELD_GROUP_CREATE: 'baseCustomFieldGroupCreate', - BASE_CUSTOM_FIELD_GROUP_UPDATE: 'baseCustomFieldGroupUpdate', - BASE_CUSTOM_FIELD_GROUP_DELETE: 'baseCustomFieldGroupDelete', - - BOARD_CREATE: 'boardCreate', - BOARD_UPDATE: 'boardUpdate', - BOARD_DELETE: 'boardDelete', - - BOARD_MEMBERSHIP_CREATE: 'boardMembershipCreate', - BOARD_MEMBERSHIP_UPDATE: 'boardMembershipUpdate', - BOARD_MEMBERSHIP_DELETE: 'boardMembershipDelete', - - CARD_CREATE: 'cardCreate', - CARD_UPDATE: 'cardUpdate', - CARD_DELETE: 'cardDelete', - - CARD_LABEL_CREATE: 'cardLabelCreate', - CARD_LABEL_DELETE: 'cardLabelDelete', - - CARD_MEMBERSHIP_CREATE: 'cardMembershipCreate', - CARD_MEMBERSHIP_DELETE: 'cardMembershipDelete', - - COMMENT_CREATE: 'commentCreate', - COMMENT_UPDATE: 'commentUpdate', - COMMENT_DELETE: 'commentDelete', - - CUSTOM_FIELD_CREATE: 'customFieldCreate', - CUSTOM_FIELD_UPDATE: 'customFieldUpdate', - CUSTOM_FIELD_DELETE: 'customFieldDelete', - - CUSTOM_FIELD_GROUP_CREATE: 'customFieldGroupCreate', - CUSTOM_FIELD_GROUP_UPDATE: 'customFieldGroupUpdate', - CUSTOM_FIELD_GROUP_DELETE: 'customFieldGroupDelete', - - CUSTOM_FIELD_VALUE_UPDATE: 'customFieldValueUpdate', - CUSTOM_FIELD_VALUE_DELETE: 'customFieldValueDelete', - - LABEL_CREATE: 'labelCreate', - LABEL_UPDATE: 'labelUpdate', - LABEL_DELETE: 'labelDelete', - - LIST_CREATE: 'listCreate', - LIST_UPDATE: 'listUpdate', - LIST_CLEAR: 'listClear', - LIST_DELETE: 'listDelete', - - NOTIFICATION_CREATE: 'notificationCreate', - NOTIFICATION_UPDATE: 'notificationUpdate', - - NOTIFICATION_SERVICE_CREATE: 'notificationServiceCreate', - NOTIFICATION_SERVICE_UPDATE: 'notificationServiceUpdate', - NOTIFICATION_SERVICE_DELETE: 'notificationServiceDelete', - - PROJECT_CREATE: 'projectCreate', - PROJECT_UPDATE: 'projectUpdate', - PROJECT_DELETE: 'projectDelete', - - PROJECT_MANAGER_CREATE: 'projectManagerCreate', - PROJECT_MANAGER_DELETE: 'projectManagerDelete', - - TASK_CREATE: 'taskCreate', - TASK_UPDATE: 'taskUpdate', - TASK_DELETE: 'taskDelete', - - TASK_LIST_CREATE: 'taskListCreate', - TASK_LIST_UPDATE: 'taskListUpdate', - TASK_LIST_DELETE: 'taskListDelete', - - USER_CREATE: 'userCreate', - USER_UPDATE: 'userUpdate', - USER_DELETE: 'userDelete', -}; +const Webhook = require('../../models/Webhook'); /** * @typedef {Object} Included @@ -114,7 +33,7 @@ const EVENT_TYPES = { * Sends a webhook notification to a configured URL. * * @param {*} webhook - Webhook configuration. - * @param {string} event - The event (see {@link EVENT_TYPES}). + * @param {string} event - The event. * @param {Data} data - The data object containing event data and optionally included data. * @param {Data} [prevData] - The data object containing previous state of data (optional). * @param {ref} user - User object associated with the event. @@ -148,11 +67,11 @@ async function sendWebhook(webhook, event, data, prevData, user) { const message = await response.text(); sails.log.error( - `Webhook ${webhook.url} failed with status ${response.status} and message: ${message}`, + `Webhook ${webhook.name} failed with status ${response.status} and message: ${message}`, ); } } catch (error) { - sails.log.error(`Webhook ${webhook.url} failed with error: ${error}`); + sails.log.error(`Webhook ${webhook.name} failed with error: ${error}`); } } @@ -160,10 +79,14 @@ module.exports = { sync: true, inputs: { + webhooks: { + type: 'ref', + required: true, + }, event: { type: 'string', required: true, - isIn: Object.values(EVENT_TYPES), + isIn: Object.values(Webhook.Events), }, buildData: { type: 'ref', @@ -179,11 +102,7 @@ module.exports = { }, fn(inputs) { - if (!sails.config.custom.webhooks) { - return; - } - - const webhooks = sails.config.custom.webhooks.filter((webhook) => { + const webhooks = inputs.webhooks.filter((webhook) => { if (!webhook.url) { return false; } diff --git a/server/api/helpers/webhooks/create-one.js b/server/api/helpers/webhooks/create-one.js new file mode 100644 index 00000000..f90f82ce --- /dev/null +++ b/server/api/helpers/webhooks/create-one.js @@ -0,0 +1,72 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +module.exports = { + inputs: { + values: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, + request: { + type: 'ref', + }, + }, + + exits: { + limitReached: {}, + }, + + // TODO: use normalizeValues + async fn(inputs) { + const { values } = inputs; + + const webhooks = await Webhook.qm.getAll(); + + // TODO: move to config? + if (webhooks.length >= 10) { + throw 'limitReached'; + } + + if (values.events) { + values.events = _.intersection(values.events, Object.values(Webhook.Events)); + delete values.excludedEvents; + } else if (values.excludedEvents) { + values.excludedEvents = _.intersection(values.excludedEvents, Object.values(Webhook.Events)); + delete values.events; + } + + const webhook = await Webhook.qm.createOne(values); + webhooks.push(webhook); + + const scoper = sails.helpers.users.makeScoper(inputs.actorUser); + const privateUserRelatedUserIds = await scoper.getPrivateUserRelatedUserIds(); + + privateUserRelatedUserIds.forEach((userId) => { + sails.sockets.broadcast( + `user:${userId}`, + 'webhookCreate', + { + item: webhook, + }, + inputs.request, + ); + }); + + sails.helpers.utils.sendWebhooks.with({ + webhooks, + event: Webhook.Events.WEBHOOK_CREATE, + buildData: () => ({ + item: webhook, + }), + user: inputs.actorUser, + }); + + return webhook; + }, +}; diff --git a/server/api/helpers/webhooks/delete-one.js b/server/api/helpers/webhooks/delete-one.js new file mode 100644 index 00000000..7e8e5eb2 --- /dev/null +++ b/server/api/helpers/webhooks/delete-one.js @@ -0,0 +1,53 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +module.exports = { + inputs: { + record: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, + request: { + type: 'ref', + }, + }, + + async fn(inputs) { + const webhooks = await Webhook.qm.getAll(); + + const webhook = await Webhook.qm.deleteOne(inputs.record.id); + + if (webhook) { + const scoper = sails.helpers.users.makeScoper(inputs.actorUser); + const privateUserRelatedUserIds = await scoper.getPrivateUserRelatedUserIds(); + + privateUserRelatedUserIds.forEach((userId) => { + sails.sockets.broadcast( + `user:${userId}`, + 'webhookDelete', + { + item: webhook, + }, + inputs.request, + ); + }); + + sails.helpers.utils.sendWebhooks.with({ + webhooks, + event: Webhook.Events.WEBHOOK_DELETE, + buildData: () => ({ + item: webhook, + }), + user: inputs.actorUser, + }); + } + + return webhook; + }, +}; diff --git a/server/api/helpers/webhooks/update-one.js b/server/api/helpers/webhooks/update-one.js new file mode 100644 index 00000000..51655bb9 --- /dev/null +++ b/server/api/helpers/webhooks/update-one.js @@ -0,0 +1,75 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +module.exports = { + inputs: { + record: { + type: 'ref', + required: true, + }, + values: { + type: 'json', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, + request: { + type: 'ref', + }, + }, + + // TODO: use normalizeValues + async fn(inputs) { + const { values } = inputs; + + if (values.events) { + Object.assign(values, { + events: _.intersection(values.events, Object.values(Webhook.Events)), + excludedEvents: null, + }); + } else if (values.excludedEvents) { + Object.assign(values, { + events: null, + excludedEvents: _.intersection(values.excludedEvents, Object.values(Webhook.Events)), + }); + } + + const webhook = await Webhook.qm.updateOne(inputs.record.id, values); + + if (webhook) { + const scoper = sails.helpers.users.makeScoper(inputs.actorUser); + const privateUserRelatedUserIds = await scoper.getPrivateUserRelatedUserIds(); + + privateUserRelatedUserIds.forEach((userId) => { + sails.sockets.broadcast( + `user:${userId}`, + 'webhookUpdate', + { + item: webhook, + }, + inputs.request, + ); + }); + + const webhooks = await Webhook.qm.getAll(); + + sails.helpers.utils.sendWebhooks.with({ + webhooks, + event: Webhook.Events.WEBHOOK_UPDATE, + buildData: () => ({ + item: webhook, + }), + buildPrevData: () => ({ + item: inputs.record, + }), + user: inputs.actorUser, + }); + } + + return webhook; + }, +}; diff --git a/server/api/hooks/query-methods/models/Webhook.js b/server/api/hooks/query-methods/models/Webhook.js new file mode 100644 index 00000000..67bfeca1 --- /dev/null +++ b/server/api/hooks/query-methods/models/Webhook.js @@ -0,0 +1,24 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +/* Query methods */ + +const createOne = (values) => Webhook.create({ ...values }).fetch(); + +const getAll = () => Webhook.find().sort('id'); + +const getOneById = (id) => Webhook.findOne(id); + +const updateOne = (criteria, values) => Webhook.updateOne(criteria).set({ ...values }); + +const deleteOne = (criteria) => Webhook.destroyOne(criteria); + +module.exports = { + createOne, + getAll, + getOneById, + updateOne, + deleteOne, +}; diff --git a/server/api/models/Webhook.js b/server/api/models/Webhook.js new file mode 100644 index 00000000..f63b18ad --- /dev/null +++ b/server/api/models/Webhook.js @@ -0,0 +1,145 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +/** + * Webhook.js + * + * @description :: A model definition represents a database table/collection. + * @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models + */ + +const Events = { + ACTION_CREATE: 'actionCreate', + + ATTACHMENT_CREATE: 'attachmentCreate', + ATTACHMENT_UPDATE: 'attachmentUpdate', + ATTACHMENT_DELETE: 'attachmentDelete', + + BACKGROUND_IMAGE_CREATE: 'backgroundImageCreate', + BACKGROUND_IMAGE_DELETE: 'backgroundImageDelete', + + BASE_CUSTOM_FIELD_GROUP_CREATE: 'baseCustomFieldGroupCreate', + BASE_CUSTOM_FIELD_GROUP_UPDATE: 'baseCustomFieldGroupUpdate', + BASE_CUSTOM_FIELD_GROUP_DELETE: 'baseCustomFieldGroupDelete', + + BOARD_CREATE: 'boardCreate', + BOARD_UPDATE: 'boardUpdate', + BOARD_DELETE: 'boardDelete', + + BOARD_MEMBERSHIP_CREATE: 'boardMembershipCreate', + BOARD_MEMBERSHIP_UPDATE: 'boardMembershipUpdate', + BOARD_MEMBERSHIP_DELETE: 'boardMembershipDelete', + + CARD_CREATE: 'cardCreate', + CARD_UPDATE: 'cardUpdate', + CARD_DELETE: 'cardDelete', + + CARD_LABEL_CREATE: 'cardLabelCreate', + CARD_LABEL_DELETE: 'cardLabelDelete', + + CARD_MEMBERSHIP_CREATE: 'cardMembershipCreate', + CARD_MEMBERSHIP_DELETE: 'cardMembershipDelete', + + COMMENT_CREATE: 'commentCreate', + COMMENT_UPDATE: 'commentUpdate', + COMMENT_DELETE: 'commentDelete', + + CUSTOM_FIELD_CREATE: 'customFieldCreate', + CUSTOM_FIELD_UPDATE: 'customFieldUpdate', + CUSTOM_FIELD_DELETE: 'customFieldDelete', + + CUSTOM_FIELD_GROUP_CREATE: 'customFieldGroupCreate', + CUSTOM_FIELD_GROUP_UPDATE: 'customFieldGroupUpdate', + CUSTOM_FIELD_GROUP_DELETE: 'customFieldGroupDelete', + + CUSTOM_FIELD_VALUE_UPDATE: 'customFieldValueUpdate', + CUSTOM_FIELD_VALUE_DELETE: 'customFieldValueDelete', + + LABEL_CREATE: 'labelCreate', + LABEL_UPDATE: 'labelUpdate', + LABEL_DELETE: 'labelDelete', + + LIST_CREATE: 'listCreate', + LIST_UPDATE: 'listUpdate', + LIST_CLEAR: 'listClear', + LIST_DELETE: 'listDelete', + + NOTIFICATION_CREATE: 'notificationCreate', + NOTIFICATION_UPDATE: 'notificationUpdate', + + NOTIFICATION_SERVICE_CREATE: 'notificationServiceCreate', + NOTIFICATION_SERVICE_UPDATE: 'notificationServiceUpdate', + NOTIFICATION_SERVICE_DELETE: 'notificationServiceDelete', + + PROJECT_CREATE: 'projectCreate', + PROJECT_UPDATE: 'projectUpdate', + PROJECT_DELETE: 'projectDelete', + + PROJECT_MANAGER_CREATE: 'projectManagerCreate', + PROJECT_MANAGER_DELETE: 'projectManagerDelete', + + TASK_CREATE: 'taskCreate', + TASK_UPDATE: 'taskUpdate', + TASK_DELETE: 'taskDelete', + + TASK_LIST_CREATE: 'taskListCreate', + TASK_LIST_UPDATE: 'taskListUpdate', + TASK_LIST_DELETE: 'taskListDelete', + + USER_CREATE: 'userCreate', + USER_UPDATE: 'userUpdate', + USER_DELETE: 'userDelete', + + WEBHOOK_CREATE: 'webhookCreate', + WEBHOOK_UPDATE: 'webhookUpdate', + WEBHOOK_DELETE: 'webhookDelete', +}; + +module.exports = { + Events, + + attributes: { + // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ + // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ + // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝ + + name: { + type: 'string', + required: true, + }, + url: { + type: 'string', + required: true, + }, + accessToken: { + type: 'string', + isNotEmptyString: true, + allowNull: true, + columnName: 'access_token', + }, + events: { + type: 'ref', + columnType: 'text[]', + }, + excludedEvents: { + type: 'ref', + columnType: 'text[]', + columnName: 'excluded_events', + }, + + // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ + // ║╣ ║║║╠╩╗║╣ ║║╚═╗ + // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝ + + // ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ + // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗ + // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝ + + boardId: { + model: 'Board', + columnName: 'board_id', + }, + }, +}; diff --git a/server/config/custom.js b/server/config/custom.js index efc9dee5..5e78a403 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -93,6 +93,4 @@ module.exports.custom = { smtpPassword: process.env.SMTP_PASSWORD, smtpFrom: process.env.SMTP_FROM, smtpTlsRejectUnauthorized: process.env.SMTP_TLS_REJECT_UNAUTHORIZED !== 'false', - - webhooks: JSON.parse(process.env.WEBHOOKS || '[]'), // TODO: validate structure }; diff --git a/server/config/policies.js b/server/config/policies.js index 1c3e3009..a54faa93 100644 --- a/server/config/policies.js +++ b/server/config/policies.js @@ -18,6 +18,11 @@ module.exports.policies = { '*': 'is-authenticated', + 'webhooks/index': ['is-admin'], + 'webhooks/create': ['is-admin'], + 'webhooks/update': ['is-admin'], + 'webhooks/delete': ['is-admin'], + 'users/create': ['is-authenticated', 'is-admin'], 'users/delete': ['is-authenticated', 'is-admin'], diff --git a/server/config/routes.js b/server/config/routes.js index ba2292bb..acb761ff 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -64,6 +64,11 @@ function staticDirServer(prefix, dirFn) { module.exports.routes = { 'GET /api/config': 'config/show', + 'GET /api/webhooks': 'webhooks/index', + 'POST /api/webhooks': 'webhooks/create', + 'PATCH /api/webhooks/:id': 'webhooks/update', + 'DELETE /api/webhooks/:id': 'webhooks/delete', + 'POST /api/access-tokens': 'access-tokens/create', 'POST /api/access-tokens/exchange-with-oidc': 'access-tokens/exchange-with-oidc', 'DELETE /api/access-tokens/me': 'access-tokens/delete', diff --git a/server/db/migrations/20250703122452_move_webhooks_configuration_from_environment_variable_to_ui.js b/server/db/migrations/20250703122452_move_webhooks_configuration_from_environment_variable_to_ui.js new file mode 100644 index 00000000..c8906ae1 --- /dev/null +++ b/server/db/migrations/20250703122452_move_webhooks_configuration_from_environment_variable_to_ui.js @@ -0,0 +1,28 @@ +/*! + * 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.createTable('webhook', (table) => { + /* Columns */ + + table.bigInteger('id').primary().defaultTo(knex.raw('next_id()')); + + table.bigInteger('board_id'); + + table.text('name').notNullable(); + table.text('url').notNullable(); + table.text('access_token'); + table.specificType('events', 'text[]'); + table.specificType('excluded_events', 'text[]'); + + table.timestamp('created_at', true); + table.timestamp('updated_at', true); + + /* Indexes */ + + table.index('board_id'); + }); + +exports.down = (knex) => knex.schema.dropTable('webhook');