diff --git a/index.js b/index.js index 3c6dfb6..9a626cd 100644 --- a/index.js +++ b/index.js @@ -51,7 +51,13 @@ class SlackAdmin { return async function(req, res, next) { try { - const response = await oThis.validators.common(req.body, req.query, req.headers, req.method); + const response = await oThis.validators.common( + req.body, + req.rawBody, + req.query, + req.headers, + req.method + ); req.body = response.requestBody; req.query = response.requestQuery; @@ -59,7 +65,7 @@ class SlackAdmin { req.decodedParams = response.decodedParams; next(); } catch (errorMessage) { - console.error('Common middleaware error:', errorMessage); + console.error('Common middleware error:', errorMessage); return res.status(200).json('Something went wrong.'); } }; @@ -89,6 +95,7 @@ class SlackAdmin { next(); } catch (err) { + console.log('Error in interactive endpoint middleware:', err); console.error('Interactive endpoint middleware error:', JSON.stringify(err)); return res.status(200).json('something_went_wrong'); } diff --git a/lib/helpers/messageHelper.js b/lib/helpers/messageHelper.js index 8b909be..8cd099a 100644 --- a/lib/helpers/messageHelper.js +++ b/lib/helpers/messageHelper.js @@ -20,6 +20,7 @@ class SlackHelper { * @param {string} params.text * @param {array} [params.blocks] * @param {string} params.responseUrl + * @param {boolean} [params.replaceOriginal] * * @returns {Promise} */ @@ -28,6 +29,7 @@ class SlackHelper { const text = params.text; const responseUrl = params.responseUrl; const blocks = params.blocks || []; + const replaceOriginal = params.replaceOriginal || false; if (!responseUrl) { return Promise.reject( @@ -44,7 +46,8 @@ class SlackHelper { const messageObject = { response_type: responseType, text: text || '', - blocks: blocks + blocks: blocks, + replace_original: replaceOriginal }; const httpLibObj = new HttpLibrary({ resource: responseUrl, noFormattingRequired: true }); diff --git a/lib/middlewareMethods/Common.js b/lib/middlewareMethods/Common.js index cb11c92..e5d8c01 100644 --- a/lib/middlewareMethods/Common.js +++ b/lib/middlewareMethods/Common.js @@ -16,30 +16,47 @@ class CommonMiddlewares { * @param requestMethod * @returns {{requestQuery: *, internalDecodedParams: {}, decodedParams: (*|{}), requestBody: *}} */ - async CommonMiddleWareMethod(requestBody, requestQuery, requestHeaders, requestMethod) { + async CommonMiddleWareMethod( + requestBody, + requestRawBody, + requestQuery, + requestHeaders, + requestMethod + ) { let internalDecodedParams = {}, decodedParams = {}; - const requestRawBody = AssignRawBodyMiddleware.assignRawBody(requestBody); + // 1. Validate slack signature + await authenticator.validateSlackSignature(requestBody, requestRawBody, requestHeaders); + // 2. Payload formatting const formattedPayload = PayloadFormatterMiddleware.formatPayload(requestBody); requestBody.payload = formattedPayload; - const sanitisedResponse = sanitizer.sanitizeBodyAndQuery(requestBody, requestQuery); - requestBody = sanitisedResponse.requestBody; - requestQuery = sanitisedResponse.requestQuery; + // 3. Sanitize body and query + const sanitizedResponse = sanitizer.sanitizeBodyAndQuery(requestBody, requestQuery); + requestBody = sanitizedResponse.requestBody; + requestQuery = sanitizedResponse.requestQuery; + // 4. Assign request params decodedParams = AssignParamsMiddleware.assignParams(requestMethod, requestBody, requestQuery); + // 5. Extract slack params const slackParamsResponse = ExtractSlackParamsMiddleware.extractSlackParams(requestBody, internalDecodedParams); requestBody = slackParamsResponse.requestBody; internalDecodedParams = slackParamsResponse.internalDecodedParams; + // 6. Validate raw body params await authenticator.validateRawBodyParams(requestRawBody); + + // 7. Validate request headers await authenticator.validateRequestHeaders(requestHeaders); + + // 8. Validate request domain await authenticator.validateRequestDomain(requestBody); - await authenticator.validateSlackSignature(requestRawBody, requestHeaders, requestBody); - await authenticator.validateSlackUser(requestRawBody, requestHeaders, requestBody); + + // 9. Validate slack user + await authenticator.validateSlackUser(requestBody); return { decodedParams, internalDecodedParams, requestBody, requestQuery }; } diff --git a/lib/slack/Modal.js b/lib/slack/Modal.js index b882e67..bbecf75 100644 --- a/lib/slack/Modal.js +++ b/lib/slack/Modal.js @@ -158,6 +158,96 @@ class Modal { oThis.viewJSON.blocks.push(textboxJSON); } + /** + * Add date picker element to modal. + * + * @param {string} labelText + * @param {string} [initialDate] + * @param {string} [placeHolderText] + */ + addDatePicker(labelText, initialDate = '1990-04-28', placeHolderText = 'Select a date') { + const oThis = this; + + const datePickerJson = { + type: 'input', + element: { + type: 'datepicker', + initial_date: initialDate, + placeholder: { + type: 'plain_text', + text: placeHolderText, + emoji: true + } + }, + label: { + type: 'plain_text', + text: labelText, + emoji: true + } + }; + + oThis.viewJSON.blocks.push(datePickerJson); + } + + /** + * Add static select to modal. + * + * @param {string} labelText + * @param {string} optionsArray + * @param {string} defaultOption + * @param {string} placeholderText + */ + addStaticSelect(labelText, optionsArray = [], defaultOption = {}, placeholderText = 'Select an item') { + const oThis = this; + + const staticSelectJson = { + type: 'input', + element: { + type: 'static_select', + placeholder: { + type: 'plain_text', + text: placeholderText, + emoji: true + }, + options: [] + }, + label: { + type: 'plain_text', + text: labelText, + emoji: true + } + }; + + for (let index = 0; index < optionsArray.length; index++) { + const currOption = optionsArray[index]; + + staticSelectJson.element.options.push({ + text: { + type: 'plain_text', + text: currOption.text, + emoji: true + }, + value: currOption.value + }); + } + + if (optionsArray.length === 0) { + if (!CommonValidators.validateNonEmptyObject(defaultOption)) { + defaultOption = { + text: { + type: 'plain_text', + text: 'this is plain_text text', + emoji: true + }, + value: 'default-value' + } + } + staticSelectJson.element.options.push(defaultOption); + } + + oThis.viewJSON.blocks.push(staticSelectJson); + } + /** * Add check boxes to modal. * diff --git a/lib/slack/ParseViewActionsApiParams.js b/lib/slack/ParseViewActionsApiParams.js index eb133c7..7777b93 100644 --- a/lib/slack/ParseViewActionsApiParams.js +++ b/lib/slack/ParseViewActionsApiParams.js @@ -175,6 +175,14 @@ class ParseBlockActionsApiParams { oThis.finalResponse.apiParams[relevantParameter] = stateObject.selected_option.value; break; } + case 'datepicker': { + oThis.finalResponse.apiParams[relevantParameter] = stateObject.selected_date; + break; + } + case 'static_select': { + oThis.finalResponse.apiParams[relevantParameter] = stateObject.selected_option.value; + break; + } default: // Do nothing; } diff --git a/middlewares/assignRawBody.js b/middlewares/assignRawBody.js index 43b3158..7283f91 100644 --- a/middlewares/assignRawBody.js +++ b/middlewares/assignRawBody.js @@ -13,7 +13,9 @@ class AssignRawBody { */ assignRawBody(requestBody) { const oThis = this; + const requestRawBody = qs.stringify(requestBody).replace(/%20/g, '+'); + return requestRawBody; } } diff --git a/middlewares/authentication/Authenticator.js b/middlewares/authentication/Authenticator.js index efeacc2..94bd517 100644 --- a/middlewares/authentication/Authenticator.js +++ b/middlewares/authentication/Authenticator.js @@ -55,17 +55,15 @@ class Authenticator { /** * Function to validate slack signature. * - * @param {object} requestRawBody - * @param {object} requestHeaders - * @param {object} requestBody + * @param {object} req * @returns {Promise} */ - async validateSlackSignature(requestRawBody, requestHeaders, requestBody) { - const authResponse = await new ValidateSlackSignature({ - rawBody: requestRawBody, - requestHeaders: requestHeaders, - slackRequestParams: requestBody - }).perform(); + async validateSlackSignature(requestBody, requestRawBody, requestHeaders) { + const authResponse = await new ValidateSlackSignature({ + requestBody: requestBody, + requestRawBody: requestRawBody, + requestHeaders: requestHeaders + }).perform(); if (authResponse.isFailure()) { throw new Error('Invalid Slack Signature'); @@ -80,10 +78,8 @@ class Authenticator { * @param {object} requestBody * @returns {Promise} */ - async validateSlackUser(requestRawBody, requestHeaders, requestBody) { + async validateSlackUser(requestBody) { const authResponse = await new ValidateSlackUser({ - rawBody: requestRawBody, - requestHeaders: requestHeaders, slackRequestParams: requestBody }).perform(); diff --git a/middlewares/authentication/RawBodyParams.js b/middlewares/authentication/RawBodyParams.js index 15648d7..4704a4d 100644 --- a/middlewares/authentication/RawBodyParams.js +++ b/middlewares/authentication/RawBodyParams.js @@ -28,7 +28,7 @@ class ValidateRawBodyParams { const oThis = this; if (!CommonValidators.validateString(oThis.rawBody)) { - console.error(`Slack authentication failed. Invalid raw Body Input ${oThis.rawBody}`); + console.error(`Slack authentication failed. Invalid raw Body Input ${JSON.stringify(oThis.rawBody)}`); return responseHelper.error({ internal_error_identifier: 'm_a_rbp_p', api_error_identifier: 'unauthorized_api_request', diff --git a/middlewares/authentication/Signature.js b/middlewares/authentication/Signature.js index fdf4ae8..634d8ec 100644 --- a/middlewares/authentication/Signature.js +++ b/middlewares/authentication/Signature.js @@ -19,11 +19,16 @@ class ValidateSlackSignature { constructor(params) { const oThis = this; - oThis.rawBody = params.rawBody; + const requestBody = params.requestBody; + oThis.requestRawBody = params.requestRawBody; oThis.requestHeaders = params.requestHeaders; - oThis.slackRequestParams = params.slackRequestParams; - oThis.requestPayload = oThis.slackRequestParams.payload || null; + if (requestBody.payload) { + const parsedPayload = JSON.parse(requestBody.payload); + oThis.apiAppId = parsedPayload.api_app_id; + } else { + oThis.apiAppId = requestBody.api_app_id; + } } /** @@ -61,9 +66,8 @@ class ValidateSlackSignature { internal_error_identifier: 'm_a_s_p', api_error_identifier: 'unauthorized_api_request', debug_options: { - body: oThis.rawBody, + rawBody: oThis.requestRawBody, headers: oThis.requestHeaders, - slackRequestParams: oThis.slackRequestParams } }); } @@ -88,14 +92,13 @@ class ValidateSlackSignature { async _validateSignature(requestTimestamp, version, signature) { const oThis = this; - const appId = oThis.slackRequestParams.api_app_id; - const signingSecret = slackAppConstants.getSigningSecretForAppId(appId); + const signingSecret = slackAppConstants.getSigningSecretForAppId(oThis.apiAppId); - const signatureString = `${version}:${requestTimestamp}:${oThis.rawBody}`; + const signatureString = `${version}:${requestTimestamp}:${oThis.requestRawBody}`; const computedSignature = crypto - .createHmac('sha256', signingSecret) - .update(signatureString) - .digest('hex'); + .createHmac('sha256', signingSecret) + .update(signatureString, 'utf8') + .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(signature, 'utf-8'), Buffer.from(computedSignature, 'utf-8'))) { console.error(`Invalid signature :: ${signature}`);