diff --git a/connect/config.js b/connect/config.js index 057dbed..407761b 100644 --- a/connect/config.js +++ b/connect/config.js @@ -8,13 +8,6 @@ module.exports = { TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || 'https://api.topcoder-dev.com/v4', MESSAGE_API_BASE_URL: process.env.MESSAGE_API_BASE_URL || 'https://api.topcoder-dev.com/v5', - // Probably temporary variables for TopCoder role ids for 'Connect Manager', 'Connect Copilot' and 'administrator' - // These are values for development backend. For production backend they may be different. - // These variables are currently being used to retrieve above role members using API V3 `/roles` endpoint. - // As soon as this endpoint is replaced with more suitable one, these variables has to be removed if no need anymore. - CONNECT_MANAGER_ROLE_ID: 8, - CONNECT_COPILOT_ROLE_ID: 4, - ADMINISTRATOR_ROLE_ID: 1, // id of the BOT user which creates post with various events in discussions TCWEBSERVICE_ID: process.env.TCWEBSERVICE_ID || '22838965', CODERBOT_USER_ID: process.env.CODERBOT_USER_ID || 'CoderBot', diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js index 7fa03f9..d581e09 100644 --- a/connect/connectNotificationServer.js +++ b/connect/connectNotificationServer.js @@ -11,7 +11,6 @@ const _ = require('lodash'); const service = require('./service'); const { BUS_API_EVENT } = require('./constants'); const EVENTS = require('./events-config').EVENTS; -const TOPCODER_ROLE_RULES = require('./events-config').TOPCODER_ROLE_RULES; const PROJECT_ROLE_RULES = require('./events-config').PROJECT_ROLE_RULES; const PROJECT_ROLE_OWNER = require('./events-config').PROJECT_ROLE_OWNER; const emailNotificationServiceHandler = require('./notificationServices/email').handler; @@ -30,7 +29,7 @@ const getTopCoderMembersNotifications = (eventConfig) => { } const getRoleMembersPromises = eventConfig.topcoderRoles.map(topcoderRole => ( - service.getRoleMembers(TOPCODER_ROLE_RULES[topcoderRole].id) + service.getRoleMembers(topcoderRole) )); return Promise.all(getRoleMembersPromises).then((membersPerRole) => { @@ -101,6 +100,34 @@ const getNotificationsForMentionedUser = (eventConfig, content) => { }); }; +/** + * Get notifications for users obtained from originator + * + * @param {Object} eventConfig event configuration + * @param {String} originator originator userId + * + * @return {Promise} resolves to a list of notifications + */ +const getNotificationsForOriginator = (eventConfig, originator) => { + // if event doesn't have to be notified to originator, just ignore + if (!eventConfig.originator) { + return Promise.resolve([]); + } + + // if we have to send notification to the originator, + // but it's not provided in the message, then throw error + if (!originator) { + return Promise.reject(new Error('Missing originator in the event message.')); + } + + return Promise.resolve([{ + userId: originator.toString(), + contents: { + originator: true, + }, + }]); +}; + /** * Get project members notifications * @@ -307,24 +334,28 @@ const handler = (topic, message, logger, callback) => { // - check that event has everything required or throw error getNotificationsForTopicStarter(eventConfig, message.topicId), getNotificationsForUserId(eventConfig, message.userId), + getNotificationsForOriginator(eventConfig, message.originator), getNotificationsForMentionedUser(eventConfig, message.postContent), getProjectMembersNotifications(eventConfig, project), getTopCoderMembersNotifications(eventConfig), - ]).then((notificationsPerSource) => ( + ]).then((notificationsPerSource) => { // first found notification for one user will be send, the rest ignored // NOTE all userId has to be string - _.uniqBy(_.flatten(notificationsPerSource), 'userId') - )).then((notifications) => ( + logger.debug('all notifications: ', notificationsPerSource); + return _.uniqBy(_.flatten(notificationsPerSource), 'userId'); + }).then((notifications) => ( excludeNotifications(notifications, eventConfig, message, { project, }) )).then((notifications) => { allNotifications = _.filter(notifications, notification => notification.userId !== `${message.initiatorUserId}`); - if (eventConfig.includeUsers && message[eventConfig.includeUsers] && message[eventConfig.includeUsers].length>0){ - allNotifications = _.filter(allNotifications, notification => message[eventConfig.includeUsers].contains(notification.userId)); + if (eventConfig.includeUsers && message[eventConfig.includeUsers] && + message[eventConfig.includeUsers].length > 0) { + allNotifications = _.filter(allNotifications, + notification => message[eventConfig.includeUsers].includes(notification.userId)); } - + logger.debug('filtered notifications: ', allNotifications); // now let's retrieve some additional data // if message has userId such messages will likely need userHandle and user full name diff --git a/connect/constants.js b/connect/constants.js index 5625c62..bfcf725 100644 --- a/connect/constants.js +++ b/connect/constants.js @@ -30,6 +30,9 @@ module.exports = { ASSIGNED_AS_OWNER: 'notifications.connect.project.member.assignedAsOwner', INVITE_CREATED: 'notifications.connect.project.member.invite.created', INVITE_UPDATED: 'notifications.connect.project.member.invite.updated', + INVITE_REQUESTED: 'notifications.connect.project.member.invite.requested', + INVITE_APPROVED: 'notifications.connect.project.member.invite.approved', + INVITE_REJECTED: 'notifications.connect.project.member.invite.rejected', }, PROJECT: { ACTIVE: 'notifications.connect.project.active', diff --git a/connect/events-config.js b/connect/events-config.js index 98b2cdb..6545783 100644 --- a/connect/events-config.js +++ b/connect/events-config.js @@ -1,7 +1,6 @@ /** * Configuration of connect events */ -const config = require('./config'); const { BUS_API_EVENT } = require('./constants'); // project member role names @@ -9,27 +8,24 @@ const PROJECT_ROLE_OWNER = 'owner'; const PROJECT_ROLE_COPILOT = 'copilot'; const PROJECT_ROLE_MANAGER = 'manager'; const PROJECT_ROLE_MEMBER = 'member'; +const PROJECT_ROLE_ACCOUNT_MANAGER = 'account_manager'; // project member role rules const PROJECT_ROLE_RULES = { [PROJECT_ROLE_OWNER]: { role: 'customer', isPrimary: true }, [PROJECT_ROLE_COPILOT]: { role: 'copilot' }, [PROJECT_ROLE_MANAGER]: { role: 'manager' }, + [PROJECT_ROLE_ACCOUNT_MANAGER]: { role: 'account_manager' }, [PROJECT_ROLE_MEMBER]: {}, }; // TopCoder roles const ROLE_CONNECT_COPILOT = 'Connect Copilot'; const ROLE_CONNECT_MANAGER = 'Connect Manager'; +const ROLE_CONNECT_COPILOT_MANAGER = 'Connect Copilot Manager'; +const ROLE_CONNECT_ACCOUNT_MANAGER = 'Connect Account Manager'; const ROLE_ADMINISTRATOR = 'administrator'; -// TopCoder role rules -const TOPCODER_ROLE_RULES = { - [ROLE_CONNECT_COPILOT]: { id: config.CONNECT_COPILOT_ROLE_ID }, - [ROLE_CONNECT_MANAGER]: { id: config.CONNECT_MANAGER_ROLE_ID }, - [ROLE_ADMINISTRATOR]: { id: config.ADMINISTRATOR_ROLE_ID }, -}; - /** * Supported events configuration * @@ -51,13 +47,14 @@ const EVENTS = [ { type: BUS_API_EVENT.CONNECT.PROJECT.CREATED, projectRoles: [PROJECT_ROLE_OWNER], + topcoderRoles: [ROLE_CONNECT_ACCOUNT_MANAGER], exclude: { topcoderRoles: [ROLE_CONNECT_MANAGER, ROLE_ADMINISTRATOR], }, }, { type: BUS_API_EVENT.CONNECT.PROJECT.SUBMITTED_FOR_REVIEW, projectRoles: [PROJECT_ROLE_OWNER], - topcoderRoles: [ROLE_CONNECT_MANAGER, ROLE_ADMINISTRATOR], + topcoderRoles: [ROLE_CONNECT_MANAGER, ROLE_CONNECT_ACCOUNT_MANAGER, ROLE_ADMINISTRATOR], }, { type: BUS_API_EVENT.CONNECT.PROJECT.APPROVED, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER], @@ -107,6 +104,17 @@ const EVENTS = [ type: BUS_API_EVENT.CONNECT.MEMBER.INVITE_CREATED, projectRoles: [], toUserHandle: true, + }, { + type: BUS_API_EVENT.CONNECT.MEMBER.INVITE_REQUESTED, + topcoderRoles: [ROLE_CONNECT_COPILOT_MANAGER], + }, { + type: BUS_API_EVENT.CONNECT.MEMBER.INVITE_APPROVED, + toUserHandle: true, + originator: true, + }, { + type: BUS_API_EVENT.CONNECT.MEMBER.INVITE_REJECTED, + topcoderRoles: [ROLE_CONNECT_COPILOT_MANAGER], + originator: true, }, // Project activity @@ -149,7 +157,7 @@ const EVENTS = [ type: BUS_API_EVENT.CONNECT.PROJECT.FILE_UPLOADED, version: 2, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], - includeUsers: 'allowedUsers' + includeUsers: 'allowedUsers', }, { type: BUS_API_EVENT.CONNECT.PROJECT.SPECIFICATION_MODIFIED, version: 2, @@ -160,12 +168,12 @@ const EVENTS = [ }, { type: BUS_API_EVENT.CONNECT.PROJECT_PLAN.MODIFIED, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], - includeUsers: 'allowedUsers' + includeUsers: 'allowedUsers', }, { type: BUS_API_EVENT.CONNECT.PROJECT_PLAN.PROGRESS_UPDATED, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], }, - + // Phase activity { type: BUS_API_EVENT.CONNECT.PROJECT_PLAN.PHASE_ACTIVATED, @@ -200,8 +208,8 @@ const EVENTS = [ }, { type: BUS_API_EVENT.CONNECT.PROJECT_PLAN.TIMELINE_ADJUSTED, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], - includeUsers: 'allowedUsers' - } + includeUsers: 'allowedUsers', + }, ]; const EVENT_BUNDLES = { @@ -263,6 +271,9 @@ const EVENT_BUNDLES = { BUS_API_EVENT.CONNECT.MEMBER.MANAGER_JOINED, BUS_API_EVENT.CONNECT.MEMBER.REMOVED, BUS_API_EVENT.CONNECT.MEMBER.INVITE_CREATED, + BUS_API_EVENT.CONNECT.MEMBER.INVITE_REQUESTED, + BUS_API_EVENT.CONNECT.MEMBER.INVITE_APPROVED, + BUS_API_EVENT.CONNECT.MEMBER.INVITE_REJECTED, ], }, PROJECT_PLAN: { @@ -293,7 +304,6 @@ const EVENT_BUNDLES = { module.exports = { PROJECT_ROLE_RULES, - TOPCODER_ROLE_RULES, EVENTS, EVENT_BUNDLES, diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js index 5aabd96..d184919 100644 --- a/connect/notificationServices/email.js +++ b/connect/notificationServices/email.js @@ -14,49 +14,48 @@ const { BUS_API_EVENT, SCHEDULED_EVENT_PERIOD, SETTINGS_EMAIL_SERVICE_ID, - ACTIVE_USER_STATUSES + ACTIVE_USER_STATUSES, } = require('../constants'); const { EVENTS, EVENT_BUNDLES } = require('../events-config'); const helpers = require('../helpers'); const service = require('../service'); -function replacePlaceholders(term,data){ +function replacePlaceholders(term, data) { let placeholders = term.match(/<[a-zA-Z]+>/g); let ret = term; - if (placeholders && placeholders.length){ - _(placeholders).each(p=>{ - let values = _.map(data,p.slice(1, -1)); + if (placeholders && placeholders.length) { + _(placeholders).each(p => { + let values = _.map(data, p.slice(1, -1)); const total = values.length; - let replacement = values.length < 3 ? - values.join(', ') : - values.slice(0,2).join(', ') +' and ' + (total-3) +'others'; + let replacement = values.length < 3 ? + values.join(', ') : + values.slice(0, 2).join(', ') + ' and ' + (total - 3) + 'others'; ret = ret.replace(p, values.join(', ')); - }) + }); } return ret; } -function getSections(projectUserEvents){ +function getSections(projectUserEvents) { let sections = []; _.chain(projectUserEvents) .groupBy(value => getEventGroupKey(value)) - .forIn((value,key)=>{ - if (!EVENT_BUNDLES[key].groupBy){ + .forIn((value, key) => { + if (!EVENT_BUNDLES[key].groupBy) { sections.push({ - title:replacePlaceholders(EVENT_BUNDLES[key].title,_(value).map(g=>g.data.data).value()), - [key]:true, - notifications: _(value).map(v=>v.data.data).value() + title: replacePlaceholders(EVENT_BUNDLES[key].title, _(value).map(g => g.data.data).value()), + [key]: true, + notifications: _(value).map(v => v.data.data).value(), }); } else { - _.chain(value).groupBy(n=>n.data.data[EVENT_BUNDLES[key].groupBy]).forIn((groupValue,groupKey)=>{ - + _.chain(value).groupBy(n => n.data.data[EVENT_BUNDLES[key].groupBy]).forIn((groupValue, groupKey) => { let title = EVENT_BUNDLES[key].title; - title = replacePlaceholders(title,_(groupValue).map(g=>g.data.data).value()); + title = replacePlaceholders(title, _(groupValue).map(g => g.data.data).value()); sections.push({ title, - [key]:true, - notifications: _(groupValue).map(g=>g.data.data).value() + [key]: true, + notifications: _(groupValue).map(g => g.data.data).value(), }); }).value(); } @@ -92,7 +91,7 @@ function handleScheduledEvents(events, setEventsStatus) { .mapValues(projectUserEvents => ({ id: _.get(projectUserEvents, '[0].data.data.projectId'), name: _.get(projectUserEvents, '[0].data.data.projectName'), - sections: getSections(projectUserEvents) + sections: getSections(projectUserEvents), })) .values() .value(), @@ -104,7 +103,7 @@ function handleScheduledEvents(events, setEventsStatus) { // update common values for bundled email eventMessage.replyTo = config.DEFAULT_REPLY_EMAIL; - eventMessage.version="v3"; + eventMessage.version = 'v3'; eventMessage.cc = []; eventMessage.from = { name: config.REPLY_EMAIL_FROM, @@ -145,8 +144,8 @@ function getEventGroupKey(value) { .keys() .find(key => _.includes(_.get(EVENT_BUNDLES, `${key}.types`), _.get(value, 'data.data.type'))) .value(); - if (!key) return 'DEFAULT'; - return key; + if (!key) return 'DEFAULT'; + return key; } /** @@ -157,7 +156,7 @@ function getEventGroupKey(value) { */ function wrapIndividualNotification(data) { const key = getEventGroupKey(data); - const subject = replacePlaceholders(EVENT_BUNDLES[key].subject,[data.data.data]); + const subject = replacePlaceholders(EVENT_BUNDLES[key].subject, [data.data.data]); return { subject, @@ -212,7 +211,7 @@ function handler(topicName, messageJSON, notification) { // don't send email notification for inactive users, ideally we should not have generated // notifications for inactive users, however, for now handling it here as safe gaurd if (userStatus && ACTIVE_USER_STATUSES.indexOf(userStatus) === -1) { - logger.error(`Notification generated for inactive user, ignoring`); + logger.error('Notification generated for inactive user, ignoring'); return; } if (config.ENABLE_DEV_MODE === 'true') { @@ -236,16 +235,16 @@ function handler(topicName, messageJSON, notification) { emailToAffectedUser: notification.contents.userEmail === userEmail, }, recipients, - version:"v3", + version: 'v3', from: { name: notification.contents.userHandle, email: config.DEFAULT_REPLY_EMAIL, }, categories, }; - eventMessage.data[eventMessage.data.type]=true; - _.assign(eventMessage.data,notification.contents); - + eventMessage.data[eventMessage.data.type] = true; + _.assign(eventMessage.data, notification.contents); + // default values that get overridden when the notification is about topics/posts updates let reference = 'project'; @@ -256,7 +255,7 @@ function handler(topicName, messageJSON, notification) { messagingEvent = true; eventMessage.data.topicId = parseInt(messageJSON.topicId, 10); eventMessage.data.postId = messageJSON.postId ? parseInt(messageJSON.postId, 10) : null; - if (messageJSON.postContent){ + if (messageJSON.postContent) { eventMessage.data.post = helpers.markdownToHTML(messageJSON.postContent); } @@ -304,9 +303,9 @@ function handler(topicName, messageJSON, notification) { // if we find the event category for the notification, use the bundle settings from the first event if (eventBundle && eventBundle.types && eventBundle.types.length) { const firstEvtInBundle = eventBundle.types[0]; - const firstEvtBundleSettingPath = `notifications['${firstEvtInBundle}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod` + const firstEvtBundleSettingPath = `notifications['${firstEvtInBundle}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`; let firstEvtBundlePeriod = _.get(settings, firstEvtBundleSettingPath); - bundlePeriod = firstEvtBundlePeriod + bundlePeriod = firstEvtBundlePeriod; logger.debug('Assuming bundle period of first event in the event category=>', bundlePeriod); } } @@ -315,7 +314,7 @@ function handler(topicName, messageJSON, notification) { } logger.debug('bundlePeriod=>', bundlePeriod); - if (bundlePeriod && "immediately" !== bundlePeriod && !requiresImmediateAttention) { + if (bundlePeriod && 'immediately' !== bundlePeriod && !requiresImmediateAttention) { if (!SCHEDULED_EVENT_PERIOD[bundlePeriod]) { throw new Error(`User's '${notification.userId}' setting for service` + ` '${SETTINGS_EMAIL_SERVICE_ID}' option 'bundlePeriod' has unsupported value '${bundlePeriod}'.`); @@ -332,7 +331,7 @@ function handler(topicName, messageJSON, notification) { }); } else { // send single field "notificationsHTML" with the rendered template - eventMessage.data = wrapIndividualNotification({data:eventMessage}); + eventMessage.data = wrapIndividualNotification({ data: eventMessage }); console.log(eventMessage.data.contents); // send event to bus api diff --git a/connect/service.js b/connect/service.js index 850bb48..10a416c 100644 --- a/connect/service.js +++ b/connect/service.js @@ -6,6 +6,8 @@ const request = require('superagent'); const config = require('./config'); const _ = require('lodash'); +let rolesCache = null; + /** * Get project details * @@ -13,10 +15,10 @@ const _ = require('lodash'); * * @return {Promise} promise resolved to project details */ -const getProject = (projectId) => { - return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) - .then((token) => { - return request +const getProject = (projectId) => ( + M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => ( + request .get(`${config.TC_API_V4_BASE_URL}/projects/${projectId}`) .set('accept', 'application/json') .set('authorization', `Bearer ${token}`) @@ -32,24 +34,68 @@ const getProject = (projectId) => { `Failed to get project details of project id: ${projectId}.` + (errorDetails ? ' Server response: ' + errorDetails : '') ); - }); + }) + )) + .catch((err) => { + err.message = 'Error generating m2m token: ' + err.message; + throw err; }) +); + +/** + * Get role id + * + * @param {String} role role + * + * @return {Promise} promise resolved to role members ids list + */ +const getRoleId = (role) => { + if (rolesCache) { + const cachedRole = _.find(rolesCache, { roleName: role }); + if (cachedRole) { + return Promise.resolve(cachedRole.id); + } + } + return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => ( + request + .get(`${config.TC_API_V3_BASE_URL}/roles`) + .set('accept', 'application/json') + .set('authorization', `Bearer ${token}`) + .then((res) => { + if (!_.get(res, 'body.result.success')) { + throw new Error('Failed to get roles list'); + } + const roles = _.get(res, 'body.result.content'); + rolesCache = roles; + return _.find(roles, { roleName: role }).id; + }).catch((err) => { + const errorDetails = _.get(err, 'response.body.result.content.message'); + throw new Error( + `Failed to get role id for role ${role}.` + + (errorDetails ? ' Server response: ' + errorDetails : '') + ); + }) + )) .catch((err) => { err.message = 'Error generating m2m token: ' + err.message; throw err; }); }; + /** * Get role members * - * @param {String} roleId role id + * @param {String} role role * * @return {Promise} promise resolved to role members ids list */ -const getRoleMembers = (roleId) => { - return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) - .then((token) => { - return request +const getRoleMembers = (role) => ( + M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => ( + getRoleId(role) + .then(roleId => ( + request .get(`${config.TC_API_V3_BASE_URL}/roles/${roleId}?fields=subjects`) .set('accept', 'application/json') .set('authorization', `Bearer ${token}`) @@ -65,13 +111,14 @@ const getRoleMembers = (roleId) => { `Failed to get role membrs of role id: ${roleId}.` + (errorDetails ? ' Server response: ' + errorDetails : '') ); - }); - }) + }) + )) + )) .catch((err) => { err.message = 'Error generating m2m token: ' + err.message; throw err; - }); -}; + }) +); /** * Get users details by ids @@ -88,8 +135,9 @@ const getUsersById = (ids) => { throw err; }) .then((token) => { + const fields = 'fields=userId,email,handle,firstName,lastName,photoURL,status'; return request - .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,email,handle,firstName,lastName,photoURL,status&query=${query}`) + .get(`${config.TC_API_V3_BASE_URL}/members/_search?${fields}&query=${query}`) .set('accept', 'application/json') .set('authorization', `Bearer ${token}`) .then((res) => { @@ -125,8 +173,9 @@ const getUsersByHandle = (handles) => { throw err; }) .then((token) => { + const fields = 'fields=userId,handle,firstName,lastName,photoURL'; return request - .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,handle,firstName,lastName,photoURL&query=${query}`) + .get(`${config.TC_API_V3_BASE_URL}/members/_search?${fields}&query=${query}`) .set('accept', 'application/json') .set('authorization', `Bearer ${token}`) .then((res) => { @@ -154,10 +203,10 @@ const getUsersByHandle = (handles) => { * * @return {Promise} promise resolved to topic details */ -const getTopic = (topicId, logger) => { - return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) - .then((token) => { - return request +const getTopic = (topicId, logger) => ( + M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => ( + request .get(`${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`) .set('accept', 'application/json') .set('authorization', `Bearer ${token}`) @@ -175,13 +224,13 @@ const getTopic = (topicId, logger) => { `Failed to get topic details of topic id: ${topicId}.` + (errorDetails ? ' Server response: ' + errorDetails : '') ); - }); - }) + }) + )) .catch((err) => { err.message = 'Error generating m2m token: ' + err.message; throw err; - }); -}; + }) +); module.exports = { getProject, diff --git a/docs/tc-notification-server-api.postman_collection.json b/docs/tc-notification-server-api.postman_collection.json index e3b1c4e..f7c65c0 100644 --- a/docs/tc-notification-server-api.postman_collection.json +++ b/docs/tc-notification-server-api.postman_collection.json @@ -23,7 +23,7 @@ "description": "", "auth": null, "events": null, - "collection": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "folder": null, "order": [ "1b3b6480-ea94-4027-8898-f82f28e2bea6", @@ -40,6 +40,7 @@ "requests": [ { "id": "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "getSettings", "url": "{{URL}}/settings", "description": "", @@ -71,6 +72,7 @@ }, { "id": "1b3b6480-ea94-4027-8898-f82f28e2bea6", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "listNotifications - invalid read filter", "url": "{{URL}}/list?offset=0&limit=20&type=notifications.connect.project.updated&read=yes", "description": "", @@ -131,6 +133,7 @@ }, { "id": "543cab06-2c7d-4aed-8cf3-0808463254d5", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "markAllRead", "url": "{{URL}}/read", "description": "", @@ -162,6 +165,7 @@ }, { "id": "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "getSettings - invalid token", "url": "{{URL}}/settings", "description": "", @@ -193,6 +197,7 @@ }, { "id": "76779830-a8a4-4636-8c03-1801b3d1863d", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "markAsRead", "url": "{{URL}}/1/read", "description": "", @@ -226,6 +231,7 @@ "id": "cb2299a5-dac7-4c40-80c4-7b1694138354", "name": "TC API - get project", "url": "https://api.topcoder-dev.com/v4/projects/1936", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "description": "", "data": [], "dataMode": "raw", @@ -339,7 +345,7 @@ ], "cookies": [], "request": "cb2299a5-dac7-4c40-80c4-7b1694138354", - "collection": "3f30c4e3-3b7a-491b-bdb2-6629d081a452" + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452" } ], "rawModeData": "", @@ -351,6 +357,7 @@ "name": "markAsRead - not found", "url": "{{URL}}/1111111/read", "description": "", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "data": [], "dataMode": "raw", "headerData": [ @@ -380,6 +387,7 @@ { "id": "d293d2c5-230d-4f34-8c97-1adc1f2f89b4", "name": "listNotifications - invalid limit", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "url": "{{URL}}/list?offset=0&limit=abc&type=notifications.connect.project.updated", "description": "", "data": [], @@ -441,6 +449,7 @@ "id": "d57ba947-a5e7-410a-b978-76882f33c86e", "name": "updateSettings", "url": "{{URL}}/settings", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "description": "", "data": [], "dataMode": "raw", @@ -471,6 +480,7 @@ { "id": "da23d550-55b3-4f7d-9131-735956d62f6d", "name": "markAllRead - missing token", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "url": "{{URL}}/read", "description": "", "data": [], @@ -495,6 +505,7 @@ }, { "id": "f2246cf7-7aae-4ea0-9d92-1d932d340302", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "updateSettings - invalid body", "url": "{{URL}}/settings", "description": "", @@ -527,6 +538,7 @@ { "id": "f3f3a847-46f6-4059-b167-b436078fb112", "name": "listNotifications - invalid offset", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "url": "{{URL}}/list?offset=-1&limit=20&type=notifications.connect.project.updated", "description": "", "data": [], @@ -586,6 +598,7 @@ }, { "id": "fce69847-5bf8-4b07-bcaf-6352db4ba923", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "listNotifications", "url": "{{URL}}/list?offset=0&limit=20", "description": "", @@ -645,4 +658,4 @@ "pathVariables": {} } ] -} \ No newline at end of file +} diff --git a/emails/src/partials/project-team.html b/emails/src/partials/project-team.html index 733ee86..89deb86 100644 --- a/emails/src/partials/project-team.html +++ b/emails/src/partials/project-team.html @@ -21,7 +21,7 @@ -
+ @@ -60,12 +60,29 @@ {{#if [notifications.connect.project.member.invite.created]}} Hi {{userFullName}}, you are invited to join the project {{projectName}}. Please click on the button ("View project on Connect") below to join. {{/if}} + {{#if [notifications.connect.project.member.invite.requested]}} + You are requested to add {{userFullName}} as a copilot + {{/if}} + {{#if [notifications.connect.project.member.invite.approved]}} + {{#if [originator]}} + Your request to add invite the member was approved + {{else}} + Hi {{userFullName}}, you are added as a copilot + {{/if}} + {{/if}} + {{#if [notifications.connect.project.member.invite.rejected]}} + {{#if [originator]}} + Your request to add the member was refused + {{else}} + Request to add {{userFullName}} as copilot was refused + {{/if}} + {{/if}} - + {{/each}} @@ -76,7 +93,7 @@ -
+ @@ -85,9 +102,15 @@ - - View project on Connect - + {{#if notifications.[0].[notifications.connect.project.member.invite.requested]}} + + Manage project team + + {{else}} + + View project on Connect + + {{/if}} @@ -98,7 +121,7 @@ -
+ -{{/if}} \ No newline at end of file +{{/if}} diff --git a/package-lock.json b/package-lock.json index 1d9bb8a..31df2b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1894,6 +1894,14 @@ "jsbn": "0.1.1" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "editions": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", @@ -7328,6 +7336,11 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, "pug": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.3.tgz", @@ -8936,24 +8949,65 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#0a31ad6ec4af9fd554354e3dd5f2beb06afbc0a7", + "version": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab", "requires": { "auth0-js": "9.6.1", "axios": "0.12.0", "bunyan": "1.8.12", - "jsonwebtoken": "8.3.0", - "jwks-rsa": "1.3.0", + "jsonwebtoken": "8.5.0", + "jwks-rsa": "1.4.0", "le_node": "1.7.1", "lodash": "4.17.10", - "millisecond": "0.1.2" + "millisecond": "0.1.2", + "request": "2.88.0" }, "dependencies": { + "ajv": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", + "requires": { + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" + } + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "6.9.1", + "har-schema": "2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "jsonwebtoken": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz", - "integrity": "sha512-oge/hvlmeJCH+iIz1DwcO7vKPkNGJHhgkspk8OH3VKlw+mbi42WtD4ig1+VXRln765vxptAv+xT26Fd3cteqag==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", + "integrity": "sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA==", "requires": { - "jws": "3.1.5", + "jws": "3.2.1", "lodash.includes": "4.3.0", "lodash.isboolean": "3.0.3", "lodash.isinteger": "4.0.4", @@ -8961,26 +9015,122 @@ "lodash.isplainobject": "4.0.6", "lodash.isstring": "4.0.1", "lodash.once": "4.1.1", - "ms": "2.1.1" + "ms": "2.1.1", + "semver": "5.6.0" } }, - "jwks-rsa": { + "jwa": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.3.0.tgz", - "integrity": "sha512-9q+d5VffK/FvFAjuXoddrq7zQybFSINV4mcwJJExGKXGyjWWpTt3vsn/aX33aB0heY02LK0qSyicdtRK0gVTig==", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.3.0.tgz", + "integrity": "sha512-SxObIyzv9a6MYuZYaSN6DhSm9j3+qkokwvCB0/OTSV5ylPq1wUQiygZQcHT5Qlux0I5kmISx3J86TxKhuefItg==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "5.1.1" + } + }, + "jwks-rsa": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.4.0.tgz", + "integrity": "sha512-6aUc+oTuqsLwIarfq3A0FqoD5LFSgveW5JO1uX2s0J8TJuOEcDc4NIMZAmVHO8tMHDw7CwOPzXF/9QhfOpOElA==", "requires": { "@types/express-jwt": "0.0.34", "debug": "2.6.9", "limiter": "1.1.3", "lru-memoizer": "1.12.0", "ms": "2.1.1", - "request": "2.85.0" + "request": "2.88.0" + } + }, + "jws": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.1.tgz", + "integrity": "sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g==", + "requires": { + "jwa": "1.3.0", + "safe-buffer": "5.1.1" + } + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "1.38.0" } }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.8.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.1.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.22", + "oauth-sign": "0.9.0", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.4.3", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "1.1.31", + "punycode": "1.4.1" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -9337,6 +9487,21 @@ } } }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", diff --git a/src/app.js b/src/app.js index 6243b88..e2e6d0e 100644 --- a/src/app.js +++ b/src/app.js @@ -74,7 +74,7 @@ function startKafkaConsumer(handlers, notificationServiceHandlers) { }); }); - + consumer .init() .then(() => _.each(_.keys(handlers), diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js index dae9625..52772fd 100644 --- a/src/services/NotificationService.js +++ b/src/services/NotificationService.js @@ -185,9 +185,11 @@ function* listNotifications(query, userId) { if (_.keys(notificationSettings).length > 0) { // only filter out notifications types which were explicitly set to 'no' - so we return notification by default const notifications = _.keys(notificationSettings).filter((notificationType) => - notificationSettings[notificationType].web.enabled !== 'no' + !notificationSettings[notificationType] && + !notificationSettings[notificationType].web && + notificationSettings[notificationType].web.enabled === 'no' ); - filter.where.type = { $in: notifications }; + filter.where.type = { $notIn: notifications }; } if (query.type) { filter.where.type = query.type;