diff --git a/client/package-lock.json b/client/package-lock.json index db2d7de7..fbfda709 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4976,6 +4976,11 @@ } } }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -5121,11 +5126,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5138,15 +5145,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5249,7 +5259,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5259,6 +5270,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5271,17 +5283,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5298,6 +5313,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5370,7 +5386,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5380,6 +5397,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5485,6 +5503,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/client/package.json b/client/package.json index bf2d783d..b4c2d7dd 100644 --- a/client/package.json +++ b/client/package.json @@ -23,6 +23,7 @@ "axios": "^0.18.0", "connected-react-router": "4.5.0", "dotenv": "^6.1.0", + "font-awesome": "^4.7.0", "history": "^4.7.2", "js-file-download": "^0.4.4", "prop-types": "^15.6.2", diff --git a/client/src/reducers/currentBoardReducer.js b/client/src/reducers/currentBoardReducer.js index 6e66684b..a95028d6 100644 --- a/client/src/reducers/currentBoardReducer.js +++ b/client/src/reducers/currentBoardReducer.js @@ -428,7 +428,6 @@ export default function currentBoardReducer(state = initialState, action) { }, }; - default: return state; } diff --git a/server/middlewares/slack.js b/server/middlewares/slack.js new file mode 100644 index 00000000..88e9c681 --- /dev/null +++ b/server/middlewares/slack.js @@ -0,0 +1,15 @@ +/** + * Check if the token of the req is valid + */ +const isTokenValid = (req, res, next) => { + try { + if (req.param('token') === process.env.SLACK_VERIFICATION_TOKEN) next(); + return; + } catch (e) { + return res.status(500).send({ error: 'Internal server error' }); + } +}; + +module.exports = { + isTokenValid +}; diff --git a/server/models/User.js b/server/models/User.js index 693f0ba5..0c04f6d5 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -23,6 +23,7 @@ const userSchema = new mongoose.Schema({ type: mongoose.Schema.Types.ObjectId, ref: 'Team' }], + slack: [{ id: String }], github: { type: { id: String, diff --git a/server/package-lock.json b/server/package-lock.json index eef2dbd9..15569817 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -5688,6 +5688,27 @@ "passport-oauth2": "1.x.x" } }, + "passport-oauth": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/passport-oauth/-/passport-oauth-0.1.15.tgz", + "integrity": "sha1-+3Tgr+hGFL+iVsX8cWzFa7/IzsA=", + "requires": { + "oauth": "0.9.x", + "passport": "~0.1.1", + "pkginfo": "0.2.x" + }, + "dependencies": { + "passport": { + "version": "0.1.18", + "resolved": "http://registry.npmjs.org/passport/-/passport-0.1.18.tgz", + "integrity": "sha1-yCZEedy2QUytu2Z1LRKzfgtlJaE=", + "requires": { + "pause": "0.0.1", + "pkginfo": "0.2.x" + } + } + } + }, "passport-oauth2": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", @@ -5699,6 +5720,15 @@ "utils-merge": "1.x.x" } }, + "passport-slack": { + "version": "0.0.7", + "resolved": "http://registry.npmjs.org/passport-slack/-/passport-slack-0.0.7.tgz", + "integrity": "sha1-Fn64Dwq2ItIVbnyucFXhbQsYkNA=", + "requires": { + "passport-oauth": "~0.1.1", + "pkginfo": "0.2.x" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -5811,6 +5841,11 @@ "find-up": "^1.0.0" } }, + "pkginfo": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.2.3.tgz", + "integrity": "sha1-cjnEKl72wwuPMoQ52bn/cQQkkPg=" + }, "pluralize": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", diff --git a/server/package.json b/server/package.json index cb6a2059..8821be3c 100644 --- a/server/package.json +++ b/server/package.json @@ -32,6 +32,7 @@ "nodemailer": "^4.6.8", "passport": "^0.4.0", "passport-github": "^1.1.0", + "passport-slack": "0.0.7", "request": "^2.88.0", "socket.io": "^2.1.1", "swagger-jsdoc": "^3.2.3", diff --git a/server/routes/index.js b/server/routes/index.js index 640649ac..e8a67357 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -5,6 +5,7 @@ const router = express.Router(); require('./boards')(router); require('./cards')(router); require('./lists')(router); +require('./slack')(router); require('./users')(router); require('./teams')(router); require('./users')(router); diff --git a/server/routes/slack.js b/server/routes/slack.js new file mode 100644 index 00000000..37b69c8b --- /dev/null +++ b/server/routes/slack.js @@ -0,0 +1,209 @@ +const axios = require('axios'); +const passport = require('passport'); +const Slack = require('../middlewares/slack'); +const Board = require('../models/Board'); +const BoardController = require('../controllers/boards'); +const CardController = require('../controllers/cards'); + +const VALIDKEYWORD = ['addCard', 'removeCard', 'getInfo', 'addLabel', 'removeLabel']; + +function parse(stringToParse) { + const retour = { + board: '', + cards: [], + keyword: '', + labels: [], + list: '', + users: [] + }; + const params = stringToParse.split(/(?= [#$&%@].*)/g); + params.forEach((param) => { + const trimedParam = param.trim(); + if (trimedParam.charAt(0) === '#' && retour.board === '')retour.board = trimedParam.substring(1); + else if (trimedParam.charAt(0) === '$')retour.cards.push(trimedParam.substring(1)); + else if (trimedParam.charAt(0) === '&')retour.labels.push(trimedParam.substring(1)); + else if (trimedParam.charAt(0) === '%' && retour.list === '')retour.list = (trimedParam.substring(1)); + else if (trimedParam.charAt(0) === '@')retour.users.push(trimedParam.substring(1)); + else if (trimedParam === params[0]) retour.keyword = trimedParam.split(' '); + }); + return retour; +} + +async function addCard(params, board, listParam) { + if (params.list === '') { + return 'You have to choose a list : addCard #boardName %listName $cardName'; + } + if (params.cards.length !== 0) { + if (!listParam && !params.keyword.includes('-R')) { + return `The list ${params.list} does not exist.`; + } + if (!listParam) { + const list = (await BoardController.postList(board.id, params.list)); + params.cards.forEach(async (card) => { + await CardController.postCard({ name: card, list: list._id }); + }); + return `You have added the cards ${params.cards.toString()} to the new list ${params.list}.`; + } + params.cards.forEach(async (card) => { + await CardController.postCard({ name: card, list: listParam._id }); + }); + return `You have add the cards ${params.cards.toString()} to ${params.list}.`; + } + return 'You have to choose a card : addCard #boardName %listName $cardName'; +} + +async function addLabel(params, board, listParam) { + if (params.list === '') { + return 'You have to choose a list : addLabel #boardName %listName $cardName &labelName'; + } + if (params.cards.length === 0) { + return 'You have to choose at least a card : addLabel #boardName %listName $cardName &labelName'; + } + if (params.labels.length === 0) { + return 'You have to choose at least a label : addLabel #boardName %listName $cardName &labelName'; + } + if (!listParam) { + return `The list ${params.list} does not exist.`; + } + await params.cards.forEach(async (card) => { + const findCard = await listParam.cards.filter(listCard => listCard.name === card); + const findLabel = await board.labels.filter(label => params.labels.includes(label.name)); + findCard.forEach((cardToLabel) => { + findLabel.forEach((labelToAdd) => { + CardController.addLabel({ cardId: cardToLabel._id, labelId: labelToAdd._id }); + }); + }); + }); + return `You have added the labels ${params.labels.toString()} to the cards ${params.cards.toString()}.`; +} + +function getInfo(params, board, listParam) { + let returnString = ''; + if (listParam && params.cards.length !== 0) { + params.cards.forEach((card) => { + const findCard = listParam.cards.filter(listCard => listCard.name === card); + findCard.forEach((findedCard) => { + returnString += `The card ${findedCard.name} description is ${findedCard.description} \n`; + }); + }); + } else if (!listParam) { + returnString += `The board ${board.name} have ${board.lists.length} list`; + board.lists.forEach((list) => { + returnString += ` \n-${list.name}`; + }); + } else { + returnString += `The list ${listParam.name} have ${listParam.cards.length} cards`; + listParam.cards.forEach((card) => { + returnString += ` \n-${card.name}`; + }); + } + return returnString; +} + +async function removeCard(params, board, listParam) { + if (params.list === '') { + return 'You have to choose a list : removeCard #boardName %listName $cardName'; + } + if (params.cards.length === 0) { + return 'You have to choose at least a card to archive : removeCard #boardName %listName $cardName'; + } + if (!listParam) { + return `The list ${params.list} does not exist.`; + } + await params.cards.forEach(async (card) => { + const findCard = await listParam.cards.filter(listCard => listCard.name === card); + findCard.forEach((cardToArchive) => { + CardController.archiveCard(cardToArchive._id); + }); + }); + return `You have archived the cards ${params.cards.toString()}.`; +} + +async function removeLabel(params, board, listParam) { + if (params.list === '') { + return 'You have to choose a list : removeLabel #boardName %listName $cardName &labelName'; + } + if (params.cards.length === 0) { + return 'You have to choose at least a card : removeLabel #boardName %listName $cardName &labelName'; + } + if (params.labels.length === 0) { + return 'You have to choose at least a label : removeLabel #boardName %listName $cardName &labelName'; + } + if (!listParam) { + return `The list ${params.list} does not exist.`; + } + await params.cards.forEach(async (card) => { + const findCard = await listParam.cards.filter(listCard => listCard.name === card); + const findLabel = await board.labels.filter(label => params.labels.includes(label.name)); + findCard.forEach((cardToLabel) => { + findLabel.forEach((labelToAdd) => { + CardController.deleteLabel({ cardId: cardToLabel._id, labelId: labelToAdd._id }); + }); + }); + }); + return `You have remove the labels ${params.labels.toString()} from the cards ${params.cards.toString()}.`; +} + +module.exports = (router) => { + router + .post('/slack/', Slack.isTokenValid, async (req, res) => { + try { + res.status(204).send(); + const params = parse(req.param('text')); + const message = async (params) => { + const boardId = await Board.findOne({ name: params.board }); + const board = await BoardController.getBoard(boardId); + if (!params.keyword[0] || !VALIDKEYWORD.includes(params.keyword[0])) { + return `No valid key-word found. Your message should begin with one of the valid key-word. + \nValid key-word are ${VALIDKEYWORD.join(', ')}`; + } + if (params.board === '') { + return 'You have to choose a board'; + } + if (!board) { + return 'Board not found.'; + } + const listParam = board.lists.find(list => list.name === params.list && list.isArchived === false); + if (params.keyword[0] === 'addCard') { + return addCard(params, board, listParam); + } + if (params.keyword[0] === 'getInfo') { + return (getInfo(params, board, listParam)); + } + if (params.keyword[0] === 'removeCard') { + return removeCard(params, board, listParam); + } + if (params.keyword[0] === 'addLabel') { + return addLabel(params, board, listParam); + } + if (params.keyword[0] === 'removeLabel') { + return removeLabel(params, board, listParam); + } + return 'Invalid request'; + }; + const result = await message(params); + + axios({ + url: req.param('response_url'), + headers: { 'Content-Type': 'application/json' }, + data: { + attachments: [{ + color: '#009cdd', + text: `${result}` + }] + }, + method: 'post', + }).then().catch(); + } catch (e) { + res.status(e.status).send({ error: e.message }); + } + }) + .get('/auth/slack', passport.authenticate('slack')) + .get('/auth/slack/callback', passport.authenticate('slack', { session: false }), async (req, res) => { + try { + console.log('callback'); + } catch (e) { + res.status(500, 'Internal server error'); + } + }); +}; diff --git a/server/validators/card.js b/server/validators/card.js index 122aa73b..5a654f9d 100644 --- a/server/validators/card.js +++ b/server/validators/card.js @@ -39,6 +39,11 @@ const updateCardName = [ .isLength({ min: 1 }), ]; +const editDate = [ + check('dueDate') + .isString(), +]; + module.exports = { addCard, addLabel,