diff --git a/.circleci/config.yml b/.circleci/config.yml index ee7fe52..51fe260 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,7 +82,7 @@ workflows: - "build-dev": filters: branches: - only: [dev, 'feature/notification-email-improvements'] + only: [dev] - "build-prod": filters: branches: diff --git a/.gitignore b/.gitignore index 9899ad4..a3583f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea node_modules *.log +log.txt .DS_Store dist diff --git a/README.md b/README.md index 38a2543..110b135 100644 --- a/README.md +++ b/README.md @@ -6,48 +6,74 @@ - Heroku Toolbelt https://toolbelt.heroku.com - git - PostgreSQL 9.5 - + ## Configuration + +### Notification server Configuration for the notification server is at `config/default.js`. The following parameters can be set in config files or in env variables: -- LOG_LEVEL: the log level -- PORT: the notification server port -- authSecret: TC auth secret -- authDomain: TC auth domain -- validIssuers: TC auth valid issuers -- jwksUri: TC auth JWKS URI -- DATABASE_URL: URI to PostgreSQL database -- DATABASE_OPTIONS: database connection options -- KAFKA_URL: comma separated Kafka hosts -- KAFKA_TOPIC_IGNORE_PREFIX: ignore this prefix for topics in the Kafka -- KAFKA_GROUP_ID: Kafka consumer group id -- KAFKA_CLIENT_CERT: Kafka connection certificate, optional; - if not provided, then SSL connection is not used, direct insecure connection is used; - if provided, it can be either path to certificate file or certificate content -- KAFKA_CLIENT_CERT_KEY: Kafka connection private key, optional; - if not provided, then SSL connection is not used, direct insecure connection is used; - if provided, it can be either path to private key file or private key content -- BUS_API_BASE_URL: Bus API url -- BUS_API_AUTH_TOKEN: Bus API auth token -- REPLY_EMAIL_PREFIX: prefix of the genereated reply email address -- REPLY_EMAIL_DOMAIN: email domain -- DEFAULT_REPLY_EMAIL: default reply to email address, for example no-reply@topcoder.com -- MENTION_EMAIL: recipient email used for email.project.post.mention event - +- **General** + - `LOG_LEVEL`: the log level + - `PORT`: the notification server port + - `DATABASE_URL`: URI to PostgreSQL database + - `DATABASE_OPTIONS`: database connection options +- **JWT authentication** + - `AUTH_SECRET`: TC auth secret + - `VALID_ISSUERS`: TC auth valid issuers + - `JWKS_URI`: TC auth JWKS URI (need only for local deployment) +- **KAFKA** + - `KAFKA_URL`: comma separated Kafka hosts + - `KAFKA_TOPIC_IGNORE_PREFIX`: ignore this prefix for topics in the Kafka + - `KAFKA_GROUP_ID`: Kafka consumer group id + - `KAFKA_CLIENT_CERT`: Kafka connection certificate, optional; + if not provided, then SSL connection is not used, direct insecure connection is used; + if provided, it can be either path to certificate file or certificate content + - `KAFKA_CLIENT_CERT_KEY`: Kafka connection private key, optional; + if not provided, then SSL connection is not used, direct insecure connection is used; + if provided, it can be either path to private key file or private key content +- **Topcoder API** + - `TC_API_V5_BASE_URL`: the TopCoder API V5 base URL +- **Notifications API** + - `API_CONTEXT_PATH`: path to serve API on +- **Machine to machine auth0 token** + - `AUTH0_URL`: auth0 URL + - `AUTH0_AUDIENCE`: auth0 audience + - `TOKEN_CACHE_TIME`: time period of the cached token + - `AUTH0_CLIENT_ID`: auth0 client id + - `AUTH0_CLIENT_SECRET`: auth0 client secret + +### Connect notification server Configuration for the connect notification server is at `connect/config.js`. The following parameters can be set in config files or in env variables: -- TC_API_V3_BASE_URL: the TopCoder API V3 base URL -- TC_API_V4_BASE_URL: the TopCoder API V4 base URL -- TC_ADMIN_TOKEN: the admin token to access TopCoder API - same for V3 and V4
- Also it has probably temporary variables of TopCoder role ids for 'Connect Manager', 'Connect Copilot' and 'administrator': -- CONNECT_MANAGER_ROLE_ID: 8, -- CONNECT_COPILOT_ROLE_ID: 4, -- ADMINISTRATOR_ROLE_ID: 1
- Provided values are 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. -- TCWEBSERVICE_ID - id of the BOT user which creates post with various events in discussions - +- **Topcoder API** + - `TC_API_V3_BASE_URL`: the TopCoder API V3 base URL + - `TC_API_V4_BASE_URL`: the TopCoder API V4 base URL + - `MESSAGE_API_BASE_URL`: the TopCoder message service API base URL +- **Topcder specific**
+ Also it has probably temporary variables of TopCoder role ids for 'Connect Manager', 'Connect Copilot' and 'administrator': + - `CONNECT_MANAGER_ROLE_ID`: 8, + - `CONNECT_COPILOT_ROLE_ID`: 4, + - `ADMINISTRATOR_ROLE_ID`: 1
+ Provided values are 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. + - `TCWEBSERVICE_ID` - id of the BOT user which creates post with various events in discussions +- **Machine to machine auth0 token** + - `AUTH0_URL`: auth0 URL + - `AUTH0_AUDIENCE`: auth0 audience + - `TOKEN_CACHE_TIME`: time period of the cached token + - `AUTH0_CLIENT_ID`: auth0 client id + - `AUTH0_CLIENT_SECRET`: auth0 client secret +- **Email notification service** + - `ENV`: environment variable (used to generate reply emails) + - `AUTH_SECRET`: auth secret (used to sign reply emails) + - `ENABLE_EMAILS`: if email service has to be enabled + - `ENABLE_DEV_MODE`: send all emails to the `DEV_MODE_EMAIL` email address + - `DEV_MODE_EMAIL`: address to send all email when `ENABLE_DEV_MODE` is enabled + - `MENTION_EMAIL`: recipient email used for `notifications.action.email.connect.project.post.mention` event + - `REPLY_EMAIL_PREFIX`: prefix of the genereated reply email address + - `REPLY_EMAIL_DOMAIN`: email domain + - `DEFAULT_REPLY_EMAIL`: default reply to email address, for example no-reply@topcoder.com Note that the above two configuration are separate because the common notification server config will be deployed to a NPM package, the connect notification server will use that NPM package, @@ -67,31 +93,19 @@ so we can run `node test/token 305384` to generate a token to manage notificatio The generated token is already configured in the Postman notification server API environment TOKEN variable. You may reuse it during review. - -## TC API Admin Token - -An admin token is needed to access TC API. This is already configured Postman notification -server API environment TC_ADMIN_TOKEN variable. -In case it expires, you may get a new token in this way: - -- use Chrome to browse connect.topcoder-dev.com -- open developer tools, click the Network tab -- log in with suser1 / Topcoder123, or mess / appirio123 -- once logged in, open some project, for example https://connect.topcoder-dev.com/projects/1936 and in the network inspector - look for the call to the project api and get the token from the auth header, see - http://pokit.org/get/img/68cdd34f3d205d6d9bd8bddb07bdc216.jpg - - ## Local deployment - for local development environment you can set variables as following: - - `authSecret`, `authDomain`, `validIssuers` can get from [tc-project-service config](https://github.com/topcoder-platform/tc-project-service/blob/dev/config/default.json) + - `AUTH_SECRET`,`VALID_ISSUERS` can get from [tc-project-service config](https://github.com/topcoder-platform/tc-project-service/blob/dev/config/default.json) - `PORT=4000` because **connect-app** call this port by default - - `jwksUri` - any - `KAFKA_TOPIC_IGNORE_PREFIX=joan-26673.` (with point at the end) - `TC_API_V4_BASE_URL=https://api.topcoder-dev.com/v4` - `TC_API_V3_BASE_URL=https://api.topcoder-dev.com/v3` - - `TC_ADMIN_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y` or follow section **TC API Admin Token** to obtain a new one if expired - `KAFKA_URL`, `KAFKA_CLIENT_CERT` and `KAFKA_CLIENT_CERT_KEY` get from [tc-bus-api readme](https://github.com/topcoder-platform/tc-bus-api/tree/dev) +- if you are willing to use notifications API which is hosted by the notifications server locally, you will need to use some patched `tc-core-library-js` module, which skips verification of user token. Because we don't know Topcoder `AUTH_SECRET` locally. So you can install this fork: + ``` + npm i https://github.com/maxceem/tc-core-library-js/tree/skip-validation + ``` + **WARNING** do not push package.json with this dependency as it skips users token validation. - start local PostgreSQL db, create an empty database, update the config/default.js DATABASE_URL param to point to the db - install dependencies `npm i` - run code lint check `npm run lint` @@ -130,5 +144,5 @@ In case it expires, you may get a new token in this way: ## Swagger Swagger API definition is provided at `docs/swagger_api.yaml`, -you may check it at `http://editor.swagger.io`. +you may check it at `http://editor.swagger.io`. diff --git a/config/default.js b/config/default.js index e6dfdc0..731a569 100644 --- a/config/default.js +++ b/config/default.js @@ -2,12 +2,8 @@ * The configuration file. */ module.exports = { - ENV: process.env.ENV, LOG_LEVEL: process.env.LOG_LEVEL, PORT: process.env.PORT, - authSecret: process.env.authSecret, - authDomain: process.env.authDomain, - jwksUri: process.env.jwksUri, DATABASE_URL: process.env.DATABASE_URL, DATABASE_OPTIONS: { dialect: 'postgres', @@ -21,7 +17,11 @@ module.exports = { }, }, - validIssuers: process.env.validIssuers ? process.env.validIssuers.replace(/\\"/g, '') : null, + AUTH_SECRET: process.env.authSecret, + VALID_ISSUERS: process.env.validIssuers ? process.env.validIssuers.replace(/\\"/g, '') : null, + // keep it here for dev purposes, it's only needed by modified version of tc-core-library-js + // which skips token validation when locally deployed + KAFKA_URL: process.env.KAFKA_URL, KAFKA_TOPIC_IGNORE_PREFIX: process.env.KAFKA_TOPIC_IGNORE_PREFIX, KAFKA_GROUP_ID: process.env.KAFKA_GROUP_ID, @@ -29,19 +29,16 @@ module.exports = { KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY ? process.env.KAFKA_CLIENT_CERT_KEY.replace('\\n', '\n') : null, - BUS_API_AUTH_TOKEN: process.env.BUS_API_AUTH_TOKEN, - MENTION_EMAIL: process.env.MENTION_EMAIL, - REPLY_EMAIL_PREFIX: process.env.REPLY_EMAIL_PREFIX, - REPLY_EMAIL_DOMAIN: process.env.REPLY_EMAIL_DOMAIN, - - TC_ADMIN_TOKEN: process.env.TC_ADMIN_TOKEN, - TC_API_BASE_URL: process.env.TC_API_BASE_URL || 'https://api.topcoder-dev.com', - TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || 'https://api.topcoder-dev.com/v3', - TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || 'https://api.topcoder-dev.com/v4', TC_API_V5_BASE_URL: process.env.TC_API_V5_BASE_URL || 'https://api.topcoder-dev.com/v5', - MESSAGE_API_BASE_URL: process.env.MESSAGE_API_BASE_URL || 'https://api.topcoder-dev.com/v4', - ENABLE_EMAILS: process.env.ENABLE_EMAILS || true, - ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE || true, - DEV_MODE_EMAIL: process.env.DEV_MODE_EMAIL, API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications', + + // Configuration for generating machine to machine auth0 token. + // The token will be used for calling another internal API. + AUTH0_URL: process.env.AUTH0_URL, + AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, + // The token will be cached. + // We define the time period of the cached token. + TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME || 86400000, + AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, + AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, }; diff --git a/connect/config.js b/connect/config.js index 66691ad..7ee00d7 100644 --- a/connect/config.js +++ b/connect/config.js @@ -3,11 +3,10 @@ */ module.exports = { + // TC API related variables TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || 'https://api.topcoder-dev.com/v3', 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/v4', - // eslint-disable-next-line max-len - TC_ADMIN_TOKEN: process.env.TC_ADMIN_TOKEN, + 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. @@ -16,7 +15,28 @@ module.exports = { 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', + + // Configuration for generating machine to machine auth0 token. + // The token will be used for calling another internal API. + AUTH0_URL: process.env.AUTH0_URL, + AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, + // The token will be cached. + // We define the time period of the cached token. + TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME || 86400000, + AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, + AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, + + // email notification service related variables + ENV: process.env.ENV, + AUTH_SECRET: process.env.authSecret, + ENABLE_EMAILS: process.env.ENABLE_EMAILS || true, + ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE || true, + DEV_MODE_EMAIL: process.env.DEV_MODE_EMAIL, + MENTION_EMAIL: process.env.MENTION_EMAIL, + REPLY_EMAIL_PREFIX: process.env.REPLY_EMAIL_PREFIX, + REPLY_EMAIL_DOMAIN: process.env.REPLY_EMAIL_DOMAIN, + REPLY_EMAIL_FROM: process.env.REPLY_EMAIL_FROM, + DEFAULT_REPLY_EMAIL: process.env.DEFAULT_REPLY_EMAIL, }; diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js index a5678ef..289ef1c 100644 --- a/connect/connectNotificationServer.js +++ b/connect/connectNotificationServer.js @@ -9,11 +9,12 @@ const config = require('./config'); const notificationServer = require('../index'); const _ = require('lodash'); const service = require('./service'); -const { BUS_API_EVENT } = require('../src/constants') +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; /** * Get TopCoder members notifications @@ -62,14 +63,13 @@ const getTopCoderMembersNotifications = (eventConfig) => { * @return {Promise} resolves to a list of notifications */ const getNotificationsForMentionedUser = (eventConfig, content) => { - if (!eventConfig.toMentionedUsers) { + if (!eventConfig.toMentionedUsers || !content) { return Promise.resolve([]); } let notifications = []; // eslint-disable-next-line - const regexUserHandle = /title=\"@([a-zA-Z0-9-_.{}\[\]]+)\"|\[.*\]\(.*\"\@(.*)\"\)/g; - const handles = []; + const regexUserHandle = /title=\"@([a-zA-Z0-9-_.{}\[\]]+)\"|\[.*?\]\(.*?\"\@(.*?)\"\)/g; let matches = regexUserHandle.exec(content); while (matches) { const handle = matches[1] ? matches[1].toString() : matches[2].toString(); @@ -81,18 +81,22 @@ const getNotificationsForMentionedUser = (eventConfig, content) => { }, }); matches = regexUserHandle.exec(content); - handles.push(handle); } // only one per userHandle notifications = _.uniqBy(notifications, 'userHandle'); return new Promise((resolve) => { - service.getUsersByHandle(handles).then((users) => { - _.map(notifications, (notification) => { - notification.userId = _.find(users, { handle: notification.userHandle }).userId.toString(); + const handles = _.map(notifications, 'userHandle'); + if (handles.length > 0) { + service.getUsersByHandle(handles).then((users) => { + _.forEach(notifications, (notification) => { + notification.userId = _.find(users, { handle: notification.userHandle }).userId.toString(); + }); + resolve(notifications); }); - resolve(notifications); - }); + } else { + resolve([]); + } }); }; @@ -243,6 +247,7 @@ const excludeNotifications = (notifications, eventConfig, message, data) => { return Promise.all([ getNotificationsForTopicStarter(excludeEventConfig, message.topicId), getNotificationsForUserId(excludeEventConfig, message.userId), + getNotificationsForMentionedUser(eventConfig, message.postContent), getProjectMembersNotifications(excludeEventConfig, project), getTopCoderMembersNotifications(excludeEventConfig), ]).then((notificationsPerSource) => ( @@ -296,7 +301,7 @@ const handler = (topic, message, callback) => { // - check that event has everything required or throw error getNotificationsForTopicStarter(eventConfig, message.topicId), getNotificationsForUserId(eventConfig, message.userId), - message.postContent ? getNotificationsForMentionedUser(eventConfig, message.postContent) : Promise.resolve([]), + getNotificationsForMentionedUser(eventConfig, message.postContent), getProjectMembersNotifications(eventConfig, project), getTopCoderMembersNotifications(eventConfig), ]).then((notificationsPerSource) => ( @@ -344,6 +349,11 @@ EVENTS.forEach(eventConfig => { notificationServer.addTopicHandler(eventConfig.type, handler); }); +// add notification service handlers +if (config.ENABLE_EMAILS) { + notificationServer.addNotificationServiceHandler(emailNotificationServiceHandler); +} + // init database, it will clear and re-create all tables notificationServer .initDatabase() diff --git a/src/constants.js b/connect/constants.js similarity index 63% rename from src/constants.js rename to connect/constants.js index fda41c5..3936d31 100644 --- a/src/constants.js +++ b/connect/constants.js @@ -1,6 +1,17 @@ module.exports = { + // periods of time in cron format (node-cron) + SCHEDULED_EVENT_PERIOD: { + every10minutes: '*/10 * * * *', + hourly: '0 * * * *', + daily: '0 7 * * *', // every day at 7am + weekly: '0 7 * * 6', // every Saturday at 7am + }, + + // email service id for settings + SETTINGS_EMAIL_SERVICE_ID: 'email', + BUS_API_EVENT: { - CONNECT : { + CONNECT: { TOPIC_CREATED: 'notifications.connect.project.topic.created', TOPIC_DELETED: 'notifications.connect.project.topic.deleted', POST_CREATED: 'notifications.connect.project.post.created', @@ -8,10 +19,11 @@ module.exports = { POST_DELETED: 'notifications.connect.project.post.deleted', MENTIONED_IN_POST: 'notifications.connect.project.post.mention', }, - EMAIL : { + EMAIL: { TOPIC_CREATED: 'notifications.action.email.connect.project.topic.created', POST_CREATED: 'notifications.action.email.connect.project.post.created', MENTIONED_IN_POST: 'notifications.action.email.connect.project.post.mention', + BUNDLED: 'notifications.action.email.connect.project.bundled', }, }, }; diff --git a/connect/events-config.js b/connect/events-config.js index 511ef0d..cb2ee33 100644 --- a/connect/events-config.js +++ b/connect/events-config.js @@ -109,6 +109,7 @@ const EVENTS = [ type: 'notifications.connect.project.topic.created', version: 2, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], + toMentionedUsers: true, }, { type: 'notifications.connect.project.post.created', version: 2, diff --git a/connect/helpers.js b/connect/helpers.js new file mode 100644 index 0000000..3092323 --- /dev/null +++ b/connect/helpers.js @@ -0,0 +1,48 @@ +/** + * Helper functions + */ +const Remarkable = require('remarkable'); + +/** + * Convert markdown into raw draftjs state + * + * @param {String} markdown - markdown to convert into raw draftjs object + * @param {Object} options - optional additional data + * + * @return {Object} ContentState +**/ +const markdownToHTML = (markdown) => { + const md = new Remarkable('full', { + html: true, + linkify: true, + // typographer: true, + }); + + // Replace the BBCode [u][/u] to markdown '++' for underline style + const _markdown = markdown.replace(new RegExp('\\[/?u\\]', 'g'), '++'); + + // remarkable js takes markdown and makes it an array of style objects for us to easily parse + return md.render(_markdown, {}); +}; + +/** + * Helper method to clean up the provided email address for deducing the final address that matters for + * the delivery of the email i.e. removing any non standard parts in the email address e.g. getting rid + * of anything after + sign in the local part of the email. + * + * @param {String} email email address to be sanitized + * + * @returns {String} sanitized email + */ +const sanitizeEmail = (email) => { + if (email) { + return email.substring(0, email.indexOf('+') !== -1 ? email.indexOf('+') : email.indexOf('@')) + + email.substring(email.indexOf('@')); + } + return ''; +}; + +module.exports = { + markdownToHTML, + sanitizeEmail, +}; diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js new file mode 100644 index 0000000..2f6f4c9 --- /dev/null +++ b/connect/notificationServices/email.js @@ -0,0 +1,224 @@ +/** + * Email notification service + */ +const _ = require('lodash'); +const jwt = require('jsonwebtoken'); +const co = require('co'); +const { logger, busService, eventScheduler, notificationService } = require('../../index'); +const { createEventScheduler, SCHEDULED_EVENT_STATUS } = eventScheduler; + +const config = require('../config'); +const { BUS_API_EVENT, SCHEDULED_EVENT_PERIOD, SETTINGS_EMAIL_SERVICE_ID } = require('../constants'); +const helpers = require('../helpers'); +const service = require('../service'); + +/** + * Handles due events which are passed by scheduler + * + * Groups events by users, bundles them and sends using Bus API + * + * @param {Array} events due events + * @param {Function} setEventsStatus function which sets statuses of processed events + */ +function handleScheduledEvents(events, setEventsStatus) { + // do nothing if there are no due events + if (events.length === 0) { + return; + } + + const eventsByUsers = _.groupBy(events, 'userId'); + + _.values(eventsByUsers).forEach((userEvents) => { + // clone data to avoid circular object + // we use common data from the first event + const eventMessage = _.clone(userEvents[0].data); + + // update common values for bundled email + eventMessage.replyTo = config.DEFAULT_REPLY_EMAIL; + eventMessage.cc = []; + eventMessage.from = { + name: config.DEFAULT_REPLY_EMAIL, + email: config.DEFAULT_REPLY_EMAIL, + }; + + // TODO: consider using templating engine to format the bundle email + // until there is Sendgrid support for loops in email templates + let emailBody = '

Your recent updates on Topcoder Connect

'; + const eventsByTopics = _.groupBy(userEvents, 'data.data.topicId'); + emailBody += ''; + + // data property we define as an array of data from each individual event + eventMessage.data = { notificationsHTML: emailBody }; + + busService.postEvent({ + topic: BUS_API_EVENT.EMAIL.BUNDLED, + originator: 'tc-notifications', + timestamp: (new Date()).toISOString(), + 'mime-type': 'application/json', + payload: eventMessage, + }).then(() => { + logger.info(`Successfully sent ${BUS_API_EVENT.EMAIL.BUNDLED} event` + + ` with body ${JSON.stringify(eventMessage)} to bus api`); + + setEventsStatus(userEvents, SCHEDULED_EVENT_STATUS.COMPLETED); + }).catch(() => { + logger.error(`Failed to send ${BUS_API_EVENT.EMAIL.BUNDLED} event` + + ` with body ${JSON.stringify(eventMessage)} to bus api`); + + setEventsStatus(userEvents, SCHEDULED_EVENT_STATUS.FAILED); + }); + }); +} + +// create and initialize scheduler +const scheduler = createEventScheduler( + BUS_API_EVENT.EMAIL.BUNDLED, + SCHEDULED_EVENT_PERIOD, + handleScheduledEvents +); + +/** + * Handler function which sends notification using email + * + * Depend on user settings it sends email immediately + * or bundles notifications for some period and send them together + * + * @param {String} topicName topic name (event type) + * @param {Object} messageJSON message raw JSON + * @param {Object} notification pre-processed notification object + */ +function handler(topicName, messageJSON, notification) { + // if it's interesting event, create email event and send to bus api + const notificationType = notification.newType || topicName; + logger.debug(`checking ${notificationType} notification ${JSON.stringify(notification)}`); + let eventType; + + if (notificationType === BUS_API_EVENT.CONNECT.TOPIC_CREATED) { + eventType = BUS_API_EVENT.EMAIL.TOPIC_CREATED; + } else if (notificationType === BUS_API_EVENT.CONNECT.POST_CREATED) { + eventType = BUS_API_EVENT.EMAIL.POST_CREATED; + } else if (notificationType === BUS_API_EVENT.CONNECT.MENTIONED_IN_POST) { + eventType = BUS_API_EVENT.EMAIL.MENTIONED_IN_POST; + } + + if (!!eventType) { + return co(function* () { + const settings = yield notificationService.getSettings(notification.userId); + + // if email notification is explicitly disabled for current notification type do nothing + // by default we treat all notification types enabled + if (settings.notifications[notificationType] + && settings.notifications[notificationType][SETTINGS_EMAIL_SERVICE_ID] + && settings.notifications[notificationType][SETTINGS_EMAIL_SERVICE_ID].enabled === 'no' + ) { + logger.verbose(`Notification '${notificationType}' won't be sent by '${SETTINGS_EMAIL_SERVICE_ID}'` + + ` service to the userId '${notification.userId}' due to his notification settings.`); + return; + } + + const topicId = parseInt(messageJSON.topicId, 10); + const postId = messageJSON.postId ? parseInt(messageJSON.postId, 10) : null; + + const users = yield service.getUsersById([notification.userId]); + logger.debug(`got users ${JSON.stringify(users)}`); + + const connectTopic = yield service.getTopic(topicId, logger); + logger.debug(`got topic ${JSON.stringify(connectTopic)}`); + + const user = users[0]; + let userEmail = user.email; + if (config.ENABLE_DEV_MODE === 'true') { + userEmail = config.DEV_MODE_EMAIL; + } + const recipients = [userEmail]; + const cc = []; + if (eventType === BUS_API_EVENT.EMAIL.MENTIONED_IN_POST) { + cc.push(config.MENTION_EMAIL); + } + const categories = [`${config.ENV}:${eventType}`.toLowerCase()]; + + // get jwt token then encode it with base64 + const body = { + userId: parseInt(notification.userId, 10), + topicId, + userEmail: helpers.sanitizeEmail(user.email), + }; + logger.debug('body', body); + logger.debug(`body for generating token: ${JSON.stringify(body)}`); + logger.debug(`AUTH_SECRET: ${config.AUTH_SECRET.substring(-5)}`); + const token = jwt.sign(body, config.AUTH_SECRET, { noTimestamp: true }).split('.')[2]; + logger.debug(`token: ${token}`); + + const replyTo = `${config.REPLY_EMAIL_PREFIX}+${topicId}/${token}@${config.REPLY_EMAIL_DOMAIN}`; + + const eventMessage = { + data: { + name: user.firstName + ' ' + user.lastName, + handle: user.handle, + topicTitle: connectTopic.title || '', + post: helpers.markdownToHTML(messageJSON.postContent), + date: (new Date()).toISOString(), + projectName: notification.contents.projectName, + projectId: messageJSON.projectId, + topicId, + postId, + authorHandle: notification.contents.userHandle, + }, + recipients, + replyTo, + cc, + from: { + name: notification.contents.userHandle, + email: config.REPLY_EMAIL_FROM, + }, + categories, + }; + + // if notifications has to be bundled + const bundlePeriod = settings.services[SETTINGS_EMAIL_SERVICE_ID] + && settings.services[SETTINGS_EMAIL_SERVICE_ID].bundlePeriod; + + if (bundlePeriod) { + 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}'.`); + } + + // schedule event to be send later + scheduler.addEvent({ + data: eventMessage, + period: bundlePeriod, + userId: notification.userId, + eventType, + reference: 'topic', + referenceId: topicId, + }); + } else { + // send event to bus api + return busService.postEvent({ + topic: eventType, + originator: 'tc-notifications', + timestamp: (new Date()).toISOString(), + 'mime-type': 'application/json', + payload: eventMessage, + }).then(() => { + logger.info(`Successfully sent ${eventType} event with body ${JSON.stringify(eventMessage)} to bus api`); + }); + } + }); + } + + // if no need to send emails, return resolved promise for consistency + return Promise.resolve(); +} + +module.exports = { + handler, +}; diff --git a/connect/service.js b/connect/service.js index 3156895..20948ef 100644 --- a/connect/service.js +++ b/connect/service.js @@ -1,9 +1,11 @@ /** * Service to get data from TopCoder API */ +/* global M2m */ const request = require('superagent'); const config = require('./config'); const _ = require('lodash'); +const { logger } = require('../index'); /** * Get project details @@ -12,26 +14,32 @@ const _ = require('lodash'); * * @return {Promise} promise resolved to project details */ -const getProject = (projectId) => request - .get(`${config.TC_API_V4_BASE_URL}/projects/${projectId}`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get project details of project id: ${projectId}`); - } - - const project = _.get(res, 'body.result.content'); - - return project; - }).catch((err) => { - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get project details of project id: ${projectId}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); - +const getProject = (projectId) => { + return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => { + return request + .get(`${config.TC_API_V4_BASE_URL}/projects/${projectId}`) + .set('accept', 'application/json') + .set('authorization', `Bearer ${token}`) + .then((res) => { + if (!_.get(res, 'body.result.success')) { + throw new Error(`Failed to get project details of project id: ${projectId}`); + } + const project = _.get(res, 'body.result.content'); + return project; + }).catch((err) => { + const errorDetails = _.get(err, 'response.body.result.content.message'); + throw new Error( + `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 members * @@ -39,25 +47,32 @@ const getProject = (projectId) => request * * @return {Promise} promise resolved to role members ids list */ -const getRoleMembers = (roleId) => request - .get(`${config.TC_API_V3_BASE_URL}/roles/${roleId}?fields=subjects`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get role memebrs of role id: ${roleId}`); - } - - const members = _.get(res, 'body.result.content.subjects'); - - return members; - }).catch((err) => { - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get role memebrs of role id: ${roleId}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); +const getRoleMembers = (roleId) => { + return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => { + return request + .get(`${config.TC_API_V3_BASE_URL}/roles/${roleId}?fields=subjects`) + .set('accept', 'application/json') + .set('authorization', `Bearer ${token}`) + .then((res) => { + if (!_.get(res, 'body.result.success')) { + throw new Error(`Failed to get role membrs of role id: ${roleId}`); + } + const members = _.get(res, 'body.result.content.subjects'); + return members; + }).catch((err) => { + const errorDetails = _.get(err, 'response.body.result.content.message'); + throw new Error( + `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 @@ -68,23 +83,31 @@ const getRoleMembers = (roleId) => request */ const getUsersById = (ids) => { const query = _.map(ids, (id) => 'userId:' + id).join(' OR '); - return request - .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,email,handle,firstName,lastName&query=${query}`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get users by id: ${ids}`); - } + return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .catch((err) => { + err.message = 'Error generating m2m token: ' + err.message; + throw err; + }) + .then((token) => { + return request + .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,email,handle,firstName,lastName&query=${query}`) + .set('accept', 'application/json') + .set('authorization', `Bearer ${token}`) + .then((res) => { + if (!_.get(res, 'body.result.success')) { + throw new Error(`Failed to get users by ids: ${ids}`); + } - const users = _.get(res, 'body.result.content'); - return users; - }).catch((err) => { - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get users by ids: ${ids}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); + const users = _.get(res, 'body.result.content'); + return users; + }).catch((err) => { + const errorDetails = _.get(err, 'response.body.result.content.message') + || `Status code: ${err.response.statusCode}`; + throw new Error( + `Failed to get users by ids: ${ids}.` + + (errorDetails ? ' Server response: ' + errorDetails : '') + ); + }); }); }; @@ -97,24 +120,31 @@ const getUsersById = (ids) => { */ const getUsersByHandle = (handles) => { const query = _.map(handles, (handle) => 'handle:' + handle).join(' OR '); - return request - .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,handle,firstName,lastName&query=${query}`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get users by handle: ${handles}`); - } - - const users = _.get(res, 'body.result.content'); + return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .catch((err) => { + err.message = 'Error generating m2m token: ' + err.message; + throw err; + }) + .then((token) => { + return request + .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,handle,firstName,lastName&query=${query}`) + .set('accept', 'application/json') + .set('authorization', `Bearer ${token}`) + .then((res) => { + if (!_.get(res, 'body.result.success')) { + throw new Error(`Failed to get users by handle: ${handles}`); + } + const users = _.get(res, 'body.result.content'); - return users; - }).catch((err) => { - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get users by handles: ${handles}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); + return users; + }).catch((err) => { + const errorDetails = _.get(err, 'response.body.result.content.message') + || `Status code: ${err.response.statusCode}`; + throw new Error( + `Failed to get users by handles: ${handles}.` + + (errorDetails ? ' Server response: ' + errorDetails : '') + ); + }); }); }; @@ -125,26 +155,34 @@ const getUsersByHandle = (handles) => { * * @return {Promise} promise resolved to topic details */ -const getTopic = (topicId, logger) => request - .get(`${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get topic details of topic id: ${topicId}`); - } - - return _.get(res, 'body.result.content'); - }).catch((err) => { - if (logger) { - logger.error(err, `Error while calling ${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`); - } - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get topic details of topic id: ${topicId}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); +const getTopic = (topicId, logger) => { + return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => { + return request + .get(`${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`) + .set('accept', 'application/json') + .set('authorization', `Bearer ${token}`) + .then((res) => { + if (!_.get(res, 'body.result.success')) { + throw new Error(`Failed to get topic details of topic id: ${topicId}`); + } + return _.get(res, 'body.result.content'); + }).catch((err) => { + if (logger) { + logger.error(err, `Error while calling ${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`); + } + const errorDetails = _.get(err, 'response.body.result.content.message'); + throw new Error( + `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/deploy.sh b/deploy.sh index cfb915e..f24ff24 100755 --- a/deploy.sh +++ b/deploy.sh @@ -37,17 +37,13 @@ KAFKA_GROUP_ID=$(eval "echo \$${ENV}_KAFKA_GROUP_ID") KAFKA_TOPIC_IGNORE_PREFIX=$(eval "echo \$${ENV}_KAFKA_TOPIC_IGNORE_PREFIX") KAFKA_URL=$(eval "echo \$${ENV}_KAFKA_URL") AUTHSECRET=$(eval "echo \$${ENV}_AUTHSECRET") -AUTHDOMAIN=$(eval "echo \$${ENV}_AUTHDOMAIN") VALID_ISSUERS=$(eval "echo \$${ENV}_VALID_ISSUERS") -JWKSURI=$(eval "echo \$${ENV}_JWKSURI") TC_API_BASE_URL=$(eval "echo \$${ENV}_TC_API_BASE_URL") -TC_ADMIN_TOKEN=$(eval "echo \$${ENV}_TC_ADMIN_TOKEN") LOG_LEVEL=$(eval "echo \$${ENV}_LOG_LEVEL") PORT=$(eval "echo \$${ENV}_PORT") # email notifications config ENABLE_EMAILS=$(eval "echo \$${ENV}_ENABLE_EMAILS") -BUS_API_AUTH_TOKEN=$(eval "echo \$${ENV}_BUS_API_AUTH_TOKEN") MENTION_EMAIL=$(eval "echo \$${ENV}_MENTION_EMAIL") REPLY_EMAIL_PREFIX=$(eval "echo \$${ENV}_REPLY_EMAIL_PREFIX") REPLY_EMAIL_DOMAIN=$(eval "echo \$${ENV}_REPLY_EMAIL_DOMAIN") @@ -71,6 +67,12 @@ AWS_ECS_CONTAINER_NAME=$(eval "echo \$${ENV}_AWS_ECS_CONTAINER_NAME") API_CONTEXT_PATH=$(eval "echo \$${ENV}_API_CONTEXT_PATH") +AUTH0_URL=$(eval "echo \$${ENV}_AUTH0_URL") +AUTH0_AUDIENCE=$(eval "echo \$${ENV}_AUTH0_AUDIENCE") +TOKEN_CACHE_TIME=$(eval "echo \$${ENV}_TOKEN_CACHE_TIME") +AUTH0_CLIENT_ID=$(eval "echo \$${ENV}_AUTH0_CLIENT_ID") +AUTH0_CLIENT_SECRET=$(eval "echo \$${ENV}_AUTH0_CLIENT_SECRET") + echo $APP_NAME configure_aws_cli() { @@ -111,141 +113,145 @@ deploy_cluster() { make_task_def(){ task_template='[ - { - "name": "%s", - "image": "%s.dkr.ecr.%s.amazonaws.com/%s:%s", - "essential": true, - "memory": 500, - "cpu": 100, - "environment": [ - { - "name": "ENV", - "value": "%s" - }, - { - "name": "KAFKA_CLIENT_CERT", - "value": "%s" - }, - { - "name": "KAFKA_CLIENT_CERT_KEY", - "value": "%s" - }, - { - "name": "KAFKA_GROUP_ID", - "value": "%s" - }, - { - "name": "KAFKA_TOPIC_IGNORE_PREFIX", - "value": "%s" - }, - { - "name": "KAFKA_URL", - "value": "%s" - }, - { - "name": "DATABASE_URL", - "value": "%s" - }, - { - "name": "authSecret", - "value": "%s" - }, - { - "name": "authDomain", - "value": "%s" - }, - { - "name": "jwksUri", - "value": "%s" - }, - { - "name": "TC_API_BASE_URL", - "value": "%s" - }, - { - "name": "TC_API_V3_BASE_URL", - "value": "%s" - }, - { - "name": "TC_API_V4_BASE_URL", - "value": "%s" - }, - { - "name": "TC_API_V5_BASE_URL", - "value": "%s" - }, - { - "name": "MESSAGE_API_BASE_URL", - "value": "%s" - }, - { - "name": "TC_ADMIN_TOKEN", - "value": "%s" - }, - { - "name": "ENABLE_EMAILS", - "value": "%s" - }, - { - "name": "MENTION_EMAIL", - "value": "%s" - }, - { - "name": "REPLY_EMAIL_PREFIX", - "value": "%s" - }, - { - "name": "REPLY_EMAIL_DOMAIN", - "value": "%s" - }, - { - "name": "ENABLE_DEV_MODE", - "value": "%s" - }, - { - "name": "DEV_MODE_EMAIL", - "value": "%s" - }, - { - "name": "BUS_API_AUTH_TOKEN", - "value": "%s" - }, - { - "name": "LOG_LEVEL", - "value": "%s" - }, - { - "name": "validIssuers", - "value": "%s" - }, - { - "name": "PORT", - "value": "%s" - }, - { - "name": "API_CONTEXT_PATH", - "value": "%s" - } - ], - "portMappings": [ - { - "hostPort": 0, - "containerPort": 4000, - "protocol": "tcp" - } - ], - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "/aws/ecs/%s", - "awslogs-region": "%s", - "awslogs-stream-prefix": "%s_%s" - } - } - } - ]' + { + "name": "%s", + "image": "%s.dkr.ecr.%s.amazonaws.com/%s:%s", + "essential": true, + "memory": 500, + "cpu": 100, + "environment": [ + { + "name": "ENV", + "value": "%s" + }, + { + "name": "KAFKA_CLIENT_CERT", + "value": "%s" + }, + { + "name": "KAFKA_CLIENT_CERT_KEY", + "value": "%s" + }, + { + "name": "KAFKA_GROUP_ID", + "value": "%s" + }, + { + "name": "KAFKA_TOPIC_IGNORE_PREFIX", + "value": "%s" + }, + { + "name": "KAFKA_URL", + "value": "%s" + }, + { + "name": "DATABASE_URL", + "value": "%s" + }, + { + "name": "authSecret", + "value": "%s" + }, + { + "name": "TC_API_BASE_URL", + "value": "%s" + }, + { + "name": "TC_API_V3_BASE_URL", + "value": "%s" + }, + { + "name": "TC_API_V4_BASE_URL", + "value": "%s" + }, + { + "name": "TC_API_V5_BASE_URL", + "value": "%s" + }, + { + "name": "MESSAGE_API_BASE_URL", + "value": "%s" + }, + { + "name": "ENABLE_EMAILS", + "value": "%s" + }, + { + "name": "MENTION_EMAIL", + "value": "%s" + }, + { + "name": "REPLY_EMAIL_PREFIX", + "value": "%s" + }, + { + "name": "REPLY_EMAIL_DOMAIN", + "value": "%s" + }, + { + "name": "ENABLE_DEV_MODE", + "value": "%s" + }, + { + "name": "DEV_MODE_EMAIL", + "value": "%s" + }, + { + "name": "LOG_LEVEL", + "value": "%s" + }, + { + "name": "validIssuers", + "value": "%s" + }, + { + "name": "PORT", + "value": "%s" + }, + { + "name": "API_CONTEXT_PATH", + "value": "%s" + }, + { + "name": "AUTH0_URL", + "value": "%s" + }, + { + "name": "AUTH0_AUDIENCE", + "value": "%s" + }, + { + "name": "AUTH0_CLIENT_ID", + "value": "%s" + }, + { + "name": "AUTH0_CLIENT_SECRET", + "value": "%s" + }, + { + "name": "TOKEN_CACHE_TIME", + "value": "%s" + } + ], + "portMappings": [ + { + "hostPort": 0, + "containerPort": 4000, + "protocol": "tcp" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/aws/ecs/%s", + "awslogs-region": "%s", + "awslogs-stream-prefix": "%s_%s" + } + } + } +]' - task_def=$(printf "$task_template" $AWS_ECS_CONTAINER_NAME $AWS_ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $TAG $ENV "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID "$KAFKA_TOPIC_IGNORE_PREFIX" $KAFKA_URL $DATABASE_URL $AUTHSECRET "$AUTHDOMAIN" "$JWKSURI" $TC_API_BASE_URL $TC_API_V3_BASE_URL $TC_API_V4_BASE_URL $TC_API_V5_BASE_URL $MESSAGE_API_BASE_URL $TC_ADMIN_TOKEN $ENABLE_EMAILS $MENTION_EMAIL $REPLY_EMAIL_PREFIX $REPLY_EMAIL_DOMAIN $ENABLE_DEV_MODE $DEV_MODE_EMAIL $BUS_API_AUTH_TOKEN $LOG_LEVEL $VALID_ISSUERS $PORT "$API_CONTEXT_PATH" $AWS_ECS_CLUSTER $AWS_REGION $AWS_ECS_CLUSTER $ENV) + task_def=$(printf "$task_template" $AWS_ECS_CONTAINER_NAME $AWS_ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $TAG $ENV "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID "$KAFKA_TOPIC_IGNORE_PREFIX" $KAFKA_URL $DATABASE_URL $AUTHSECRET $TC_API_BASE_URL $TC_API_V3_BASE_URL $TC_API_V4_BASE_URL $TC_API_V5_BASE_URL $MESSAGE_API_BASE_URL $ENABLE_EMAILS $MENTION_EMAIL $REPLY_EMAIL_PREFIX $REPLY_EMAIL_DOMAIN $ENABLE_DEV_MODE $DEV_MODE_EMAIL $LOG_LEVEL $VALID_ISSUERS $PORT "$API_CONTEXT_PATH" "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME $AWS_ECS_CLUSTER $AWS_REGION $AWS_ECS_CLUSTER $ENV) } register_definition() { diff --git a/docs/tc-notification-server-api-heroku-env.postman_environment.json b/docs/tc-notification-server-api-heroku-env.postman_environment.json index 04005e3..d11474e 100644 --- a/docs/tc-notification-server-api-heroku-env.postman_environment.json +++ b/docs/tc-notification-server-api-heroku-env.postman_environment.json @@ -1,28 +1,27 @@ -{ - "id": "30bb1f5b-1185-2a81-b1c9-b95bf3c89140", - "name": "tc-notification-server-api-heroku-env", - "values": [ - { - "enabled": true, - "key": "URL", - "value": "https://serene-basin-77096.herokuapp.com", - "type": "text" - }, - { - "enabled": true, - "key": "TOKEN", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMwNTM4NCwiaXNzIjoiaHR0cHM6Ly9hcGkudG9wY29kZXItZGV2LmNvbSIsImlhdCI6MTUxMDYxODE0NiwiZXhwIjoxNTEzMjEwMTQ2fQ.Miqo8OpHIPU5kI_YwiOcVgIjTqFlLEvpOptjNzVnF8E", - "type": "text" - }, - { - "enabled": true, - "key": "TC_ADMIN_TOKEN", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y", - "type": "text" - } - ], - "timestamp": 1510810911759, - "_postman_variable_scope": "environment", - "_postman_exported_at": "2017-11-16T05:43:05.095Z", - "_postman_exported_using": "Postman/5.3.2" +{ + "id": "4a44c2b1-b1ff-4045-970e-bb2836d0634c", + "name": "tc-notification-server-api-heroku-env", + "values": [ + { + "enabled": true, + "key": "URL", + "value": "https://serene-basin-77096.herokuapp.com/v5/notifications", + "type": "text" + }, + { + "enabled": true, + "key": "TOKEN", + "value": "", + "type": "text" + }, + { + "enabled": true, + "key": "TC_ADMIN_TOKEN", + "value": "", + "type": "text" + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2018-05-14T06:13:53.899Z", + "_postman_exported_using": "Postman/6.0.10" } \ No newline at end of file diff --git a/docs/tc-notification-server-api-local-env.postman_environment.json b/docs/tc-notification-server-api-local-env.postman_environment.json index e6d0428..5158a51 100644 --- a/docs/tc-notification-server-api-local-env.postman_environment.json +++ b/docs/tc-notification-server-api-local-env.postman_environment.json @@ -1,28 +1,27 @@ -{ - "id": "6d9e5bca-6b22-ea18-e546-580f2c8fb5ff", - "name": "tc-notification-server-api-local-env", - "values": [ - { - "enabled": true, - "key": "URL", - "value": "http://localhost:4000", - "type": "text" - }, - { - "enabled": true, - "key": "TOKEN", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMwNTM4NCwiaXNzIjoiaHR0cHM6Ly9hcGkudG9wY29kZXItZGV2LmNvbSIsImlhdCI6MTUxMDYxODE0NiwiZXhwIjoxNTEzMjEwMTQ2fQ.Miqo8OpHIPU5kI_YwiOcVgIjTqFlLEvpOptjNzVnF8E", - "type": "text" - }, - { - "enabled": true, - "key": "TC_ADMIN_TOKEN", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y", - "type": "text" - } - ], - "timestamp": 1510810894543, - "_postman_variable_scope": "environment", - "_postman_exported_at": "2017-11-16T05:42:38.253Z", - "_postman_exported_using": "Postman/5.3.2" +{ + "id": "8087b254-f1d0-49f4-85ce-44fdab883b48", + "name": "tc-notification-server-api-local-env", + "values": [ + { + "enabled": true, + "key": "URL", + "value": "http://localhost:4000/v5/notifications", + "type": "text" + }, + { + "enabled": true, + "key": "TOKEN", + "value": "", + "type": "text" + }, + { + "enabled": true, + "key": "TC_ADMIN_TOKEN", + "value": "", + "type": "text" + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2018-05-14T06:12:15.589Z", + "_postman_exported_using": "Postman/6.0.10" } \ No newline at end of file diff --git a/docs/tc-notification-server-api.postman_collection.json b/docs/tc-notification-server-api.postman_collection.json index 640f0c6..e3b1c4e 100644 --- a/docs/tc-notification-server-api.postman_collection.json +++ b/docs/tc-notification-server-api.postman_collection.json @@ -1,742 +1,648 @@ -{ - "id": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "name": "tc-notification-server-api", - "description": "", - "order": [ - "e16cc911-9393-9f47-cce4-3ef583449fba", - "6e2f5e53-61db-bb45-f7ae-d0fc9883f92b", - "919e4314-ca62-918b-e790-7e09afd4cb9b", - "dc85dae6-5c2c-114a-7b29-2ddbb0310c06", - "4d9e3771-b9d3-a186-45cd-b57b665b4647", - "c8ddeee1-2851-b571-08bd-95b822632e40" - ], - "folders": [ - { - "name": "failure", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "order": [ - "36087c50-2265-6db2-7f23-d5607006bb6c", - "99bd0688-cdd3-9136-b64e-f1706884fced", - "0c7f4099-db8a-5772-9627-335600a8d70e", - "dac3727a-ed58-854d-73a2-ab8d5a2256bb", - "9c272f57-3621-7f7b-a6a9-3a26969dd4d1", - "f8bfa330-ac2e-f0fa-dae8-bb2bcdb3819f", - "0f6ffaad-aadb-8ad1-a0b1-16c4b1d87718" - ], - "owner": 0, - "folders_order": [], - "id": "c54ef790-e9db-6bac-432b-80f10e9fd76a" - } - ], - "folders_order": [ - "c54ef790-e9db-6bac-432b-80f10e9fd76a" - ], - "timestamp": 1507789221862, - "owner": 0, - "public": false, - "requests": [ - { - "id": "0c7f4099-db8a-5772-9627-335600a8d70e", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notifications?offset=0&limit=20&type=notifications.connect.project.updated&read=yes", - "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a", - "queryParams": [ - { - "key": "offset", - "value": "0", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "limit", - "value": "20", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "type", - "value": "notifications.connect.project.updated", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "read", - "value": "yes", - "equals": true, - "description": "", - "enabled": true - } - ], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "GET", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509801563428, - "name": "listNotifications - invalid read filter", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "0f6ffaad-aadb-8ad1-a0b1-16c4b1d87718", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notificationsettings", - "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "PUT", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509801716547, - "name": "updateSettings - invalid body", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "36087c50-2265-6db2-7f23-d5607006bb6c", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notifications?offset=-1&limit=20&type=notifications.connect.project.updated", - "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a", - "queryParams": [ - { - "key": "offset", - "value": "-1", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "limit", - "value": "20", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "type", - "value": "notifications.connect.project.updated", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "read", - "value": "false", - "equals": true, - "description": "", - "enabled": false - } - ], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "GET", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509801475461, - "name": "listNotifications - invalid offset", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "4d9e3771-b9d3-a186-45cd-b57b665b4647", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notificationsettings", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "GET", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509793503637, - "name": "getSettings", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "6e2f5e53-61db-bb45-f7ae-d0fc9883f92b", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notifications?offset=0&limit=20", - "queryParams": [ - { - "key": "offset", - "value": "0", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "limit", - "value": "20", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "type", - "value": "notifications.connect.project.updated", - "equals": true, - "description": "", - "enabled": false - }, - { - "key": "read", - "value": "false", - "equals": true, - "description": "", - "enabled": false - } - ], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "GET", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1510707608411, - "name": "listNotifications", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "" - }, - { - "id": "919e4314-ca62-918b-e790-7e09afd4cb9b", - "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": false - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notifications/1/read", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "PUT", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509786004930, - "name": "markAsRead", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "99bd0688-cdd3-9136-b64e-f1706884fced", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notifications?offset=0&limit=abc&type=notifications.connect.project.updated", - "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a", - "queryParams": [ - { - "key": "offset", - "value": "0", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "limit", - "value": "abc", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "type", - "value": "notifications.connect.project.updated", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "read", - "value": "false", - "equals": true, - "description": "", - "enabled": false - } - ], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "GET", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509801518188, - "name": "listNotifications - invalid limit", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "9c272f57-3621-7f7b-a6a9-3a26969dd4d1", - "headers": "//Content-Type: application/json\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": false - } - ], - "url": "{{URL}}/notifications/read", - "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "PUT", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509801636468, - "name": "markAllRead - missing token", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "c8ddeee1-2851-b571-08bd-95b822632e40", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notificationsettings", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "PUT", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509794397038, - "name": "updateSettings", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "[\n\t{\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "dac3727a-ed58-854d-73a2-ab8d5a2256bb", - "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": false - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notifications/1111111/read", - "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "PUT", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509801601644, - "name": "markAsRead - not found", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "dc85dae6-5c2c-114a-7b29-2ddbb0310c06", - "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": false - }, - { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notifications/read", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "PUT", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509793307190, - "name": "markAllRead", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "e16cc911-9393-9f47-cce4-3ef583449fba", - "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "authorization", - "value": "Bearer {{TC_ADMIN_TOKEN}}", - "description": "", - "enabled": true - } - ], - "url": "https://api.topcoder-dev.com/v4/projects/1936", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "GET", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509766333902, - "name": "TC API - get project", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [ - { - "status": "", - "responseCode": { - "code": 200, - "name": "OK", - "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action." - }, - "time": 760, - "headers": [ - { - "name": "access-control-allow-credentials", - "key": "access-control-allow-credentials", - "value": "true", - "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials." - }, - { - "name": "access-control-allow-headers", - "key": "access-control-allow-headers", - "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since", - "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." - }, - { - "name": "access-control-allow-methods", - "key": "access-control-allow-methods", - "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH", - "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." - }, - { - "name": "connection", - "key": "connection", - "value": "keep-alive", - "description": "Options that are desired for the connection" - }, - { - "name": "content-encoding", - "key": "content-encoding", - "value": "gzip", - "description": "The type of encoding used on the data." - }, - { - "name": "content-length", - "key": "content-length", - "value": "491", - "description": "The length of the response body in octets (8-bit bytes)" - }, - { - "name": "content-type", - "key": "content-type", - "value": "application/json; charset=utf-8", - "description": "The mime type of this content" - }, - { - "name": "date", - "key": "date", - "value": "Thu, 02 Nov 2017 04:28:20 GMT", - "description": "The date and time that the message was sent" - }, - { - "name": "etag", - "key": "etag", - "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"", - "description": "An identifier for a specific version of a resource, often a message digest" - }, - { - "name": "server", - "key": "server", - "value": "nginx/1.9.7", - "description": "A name for the server" - }, - { - "name": "x-powered-by", - "key": "x-powered-by", - "value": "Express", - "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" - }, - { - "name": "x-request-id", - "key": "x-request-id", - "value": "95744bd2-2830-4014-8885-7182a6225953", - "description": "Custom header" - } - ], - "cookies": [], - "mime": "", - "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}", - "language": "json", - "rawDataType": "text", - "previewType": "text", - "searchResultScrolledTo": -1, - "forceNoPretty": false, - "write": true, - "empty": false, - "failed": false, - "name": "test111", - "id": "24a44c8d-b9d3-e6ab-8439-62625dafbd07", - "request": { - "url": "http://api.topcoder-dev.com/v4/projects/1936", - "pathVariables": {}, - "pathVariableData": [], - "queryParams": [], - "headerData": [ - { - "key": "authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTA5NTk3MjM4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTU5NjYzOCwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6ImZmNWY3YWEzLWE0MDktNDE4Ny1hYTBjLWZhZDVmMjI1YTE0NyJ9.WcDvq6bS2R1CMl1YWFzyiSjo0C801RNNS6ACqVRWqWw", - "description": "", - "enabled": true, - "warning": "" - } - ], - "headers": "authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTA5NTk3MjM4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTU5NjYzOCwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6ImZmNWY3YWEzLWE0MDktNDE4Ny1hYTBjLWZhZDVmMjI1YTE0NyJ9.WcDvq6bS2R1CMl1YWFzyiSjo0C801RNNS6ACqVRWqWw\n", - "data": "", - "method": "GET", - "dataMode": "raw" - }, - "owner": 0 - } - ], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - }, - { - "id": "f8bfa330-ac2e-f0fa-dae8-bb2bcdb3819f", - "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer invalid", - "description": "", - "enabled": true - } - ], - "url": "{{URL}}/notificationsettings", - "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a", - "queryParams": [], - "preRequestScript": null, - "pathVariables": {}, - "pathVariableData": [], - "method": "GET", - "data": [], - "dataMode": "raw", - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1509801668820, - "name": "getSettings - invalid token", - "description": "", - "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e", - "responses": [], - "rawModeData": "", - "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e" - } - ] +{ + "id": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "name": "tc-notification-server-api", + "description": "", + "auth": null, + "events": null, + "variables": null, + "order": [ + "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4", + "543cab06-2c7d-4aed-8cf3-0808463254d5", + "76779830-a8a4-4636-8c03-1801b3d1863d", + "cb2299a5-dac7-4c40-80c4-7b1694138354", + "d57ba947-a5e7-410a-b978-76882f33c86e", + "fce69847-5bf8-4b07-bcaf-6352db4ba923" + ], + "folders_order": [ + "dbebd550-6c33-4778-b467-d56decf16c91" + ], + "folders": [ + { + "id": "dbebd550-6c33-4778-b467-d56decf16c91", + "name": "failure", + "description": "", + "auth": null, + "events": null, + "collection": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "folder": null, + "order": [ + "1b3b6480-ea94-4027-8898-f82f28e2bea6", + "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8", + "cbc03cb1-6dfe-43fd-8e99-8c56923c2978", + "d293d2c5-230d-4f34-8c97-1adc1f2f89b4", + "da23d550-55b3-4f7d-9131-735956d62f6d", + "f2246cf7-7aae-4ea0-9d92-1d932d340302", + "f3f3a847-46f6-4059-b167-b436078fb112" + ], + "folders_order": [] + } + ], + "requests": [ + { + "id": "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4", + "name": "getSettings", + "url": "{{URL}}/settings", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "GET", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": null, + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "1b3b6480-ea94-4027-8898-f82f28e2bea6", + "name": "listNotifications - invalid read filter", + "url": "{{URL}}/list?offset=0&limit=20&type=notifications.connect.project.updated&read=yes", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "GET", + "pathVariableData": [], + "queryParams": [ + { + "key": "offset", + "value": "0", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "limit", + "value": "20", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "type", + "value": "notifications.connect.project.updated", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "read", + "value": "yes", + "equals": true, + "description": "", + "enabled": true + } + ], + "auth": null, + "events": null, + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "543cab06-2c7d-4aed-8cf3-0808463254d5", + "name": "markAllRead", + "url": "{{URL}}/read", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": false + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "PUT", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": null, + "rawModeData": "", + "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8", + "name": "getSettings - invalid token", + "url": "{{URL}}/settings", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer invalid", + "description": "", + "enabled": true + } + ], + "method": "GET", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n", + "pathVariables": {} + }, + { + "id": "76779830-a8a4-4636-8c03-1801b3d1863d", + "name": "markAsRead", + "url": "{{URL}}/1/read", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": false + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "PUT", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": null, + "rawModeData": "", + "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "cb2299a5-dac7-4c40-80c4-7b1694138354", + "name": "TC API - get project", + "url": "https://api.topcoder-dev.com/v4/projects/1936", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "authorization", + "value": "Bearer {{TC_ADMIN_TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "GET", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": null, + "responses": [ + { + "id": "ae658c70-e29d-4d49-aefd-944af0e4f811", + "name": "test111", + "status": "", + "mime": "", + "language": "json", + "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}", + "responseCode": { + "code": 200, + "name": "OK", + "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action." + }, + "requestObject": null, + "headers": [ + { + "name": "access-control-allow-credentials", + "key": "access-control-allow-credentials", + "value": "true", + "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials." + }, + { + "name": "access-control-allow-headers", + "key": "access-control-allow-headers", + "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since", + "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." + }, + { + "name": "access-control-allow-methods", + "key": "access-control-allow-methods", + "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH", + "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." + }, + { + "name": "connection", + "key": "connection", + "value": "keep-alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-encoding", + "key": "content-encoding", + "value": "gzip", + "description": "The type of encoding used on the data." + }, + { + "name": "content-length", + "key": "content-length", + "value": "491", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json; charset=utf-8", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Thu, 02 Nov 2017 04:28:20 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "etag", + "key": "etag", + "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"", + "description": "An identifier for a specific version of a resource, often a message digest" + }, + { + "name": "server", + "key": "server", + "value": "nginx/1.9.7", + "description": "A name for the server" + }, + { + "name": "x-powered-by", + "key": "x-powered-by", + "value": "Express", + "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" + }, + { + "name": "x-request-id", + "key": "x-request-id", + "value": "95744bd2-2830-4014-8885-7182a6225953", + "description": "Custom header" + } + ], + "cookies": [], + "request": "cb2299a5-dac7-4c40-80c4-7b1694138354", + "collection": "3f30c4e3-3b7a-491b-bdb2-6629d081a452" + } + ], + "rawModeData": "", + "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "cbc03cb1-6dfe-43fd-8e99-8c56923c2978", + "name": "markAsRead - not found", + "url": "{{URL}}/1111111/read", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": false + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "PUT", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "d293d2c5-230d-4f34-8c97-1adc1f2f89b4", + "name": "listNotifications - invalid limit", + "url": "{{URL}}/list?offset=0&limit=abc&type=notifications.connect.project.updated", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "GET", + "pathVariableData": [], + "queryParams": [ + { + "key": "offset", + "value": "0", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "limit", + "value": "abc", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "type", + "value": "notifications.connect.project.updated", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "read", + "value": "false", + "equals": true, + "description": "", + "enabled": false + } + ], + "auth": null, + "events": null, + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "d57ba947-a5e7-410a-b978-76882f33c86e", + "name": "updateSettings", + "url": "{{URL}}/settings", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "PUT", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": null, + "rawModeData": "{\n \"notifications\": {\n \"notifications.connect.project.active\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.updated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.left\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.paused\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.approved\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.fileUploaded\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.canceled\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.copilotJoined\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.assignedAsOwner\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.completed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.joined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.removed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.specificationModified\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.managerJoined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.submittedForReview\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.linkCreated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.edited\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n }\n },\n \"services\": {\n \"email\": {\n \"bundlePeriod\": \"every10minutes\"\n }\n }\n}", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "da23d550-55b3-4f7d-9131-735956d62f6d", + "name": "markAllRead - missing token", + "url": "{{URL}}/read", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": false + } + ], + "method": "PUT", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "//Content-Type: application/json\n", + "pathVariables": {} + }, + { + "id": "f2246cf7-7aae-4ea0-9d92-1d932d340302", + "name": "updateSettings - invalid body", + "url": "{{URL}}/settings", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "PUT", + "pathVariableData": [], + "queryParams": [], + "auth": null, + "events": null, + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "f3f3a847-46f6-4059-b167-b436078fb112", + "name": "listNotifications - invalid offset", + "url": "{{URL}}/list?offset=-1&limit=20&type=notifications.connect.project.updated", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "GET", + "pathVariableData": [], + "queryParams": [ + { + "key": "offset", + "value": "-1", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "limit", + "value": "20", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "type", + "value": "notifications.connect.project.updated", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "read", + "value": "false", + "equals": true, + "description": "", + "enabled": false + } + ], + "auth": null, + "events": null, + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + }, + { + "id": "fce69847-5bf8-4b07-bcaf-6352db4ba923", + "name": "listNotifications", + "url": "{{URL}}/list?offset=0&limit=20", + "description": "", + "data": [], + "dataMode": "raw", + "headerData": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true + } + ], + "method": "GET", + "pathVariableData": [], + "queryParams": [ + { + "key": "offset", + "value": "0", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "limit", + "value": "20", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "type", + "value": "notifications.connect.project.updated", + "equals": true, + "description": "", + "enabled": false + }, + { + "key": "read", + "value": "false", + "equals": true, + "description": "", + "enabled": false + } + ], + "auth": null, + "events": null, + "folder": null, + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "pathVariables": {} + } + ] } \ No newline at end of file diff --git a/index.js b/index.js index 94dd81b..c13c164 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,13 @@ const config = require('config'); const _ = require('lodash'); const errors = require('./src/common/errors'); +const tcCoreLibAuth = require('tc-core-library-js').auth; +// some useful components to exposure +const logger = require('./src/common/logger'); +const busService = require('./src/services/BusAPI'); +const eventScheduler = require('./src/services/EventScheduler'); +const notificationService = require('./src/services/NotificationService'); +global.M2m = tcCoreLibAuth.m2m(config); // key is topic name, e.g. 'notifications.connect.project.created'; // value is handler for the topic to find user ids that should receive notifications for a message, @@ -15,6 +22,17 @@ const errors = require('./src/common/errors'); // the callback is function(error, userIds), where userIds is an array of user ids to receive notifications const handlers = {}; +/** + * List of notification service handlers which will process notifications + * + * Each item is the function of the next signature + * function(topicName, messageJSON, notification) + * - {String} topicName topic name (event type) + * - {Object} messageJSON message raw JSON + * - {Object} notification pre-processed notification object + */ +const notificationServiceHandlers = []; + /** * Set configuration, the default config will be overridden by the given config, * unspecified config parameters will not be changed, i.e. still using default values. @@ -45,6 +63,18 @@ function addTopicHandler(topic, handler) { handlers[topic] = handler; } +/** + * Adds notification service handler + * + * @param {Function} handler notification service handler + */ +function addNotificationServiceHandler(handler) { + if (!handler) { + throw new errors.ValidationError('Missing notification service handler.'); + } + notificationServiceHandlers.push(handler); +} + /** * Remove topic handler for topic. * @param {String} topic the topic name @@ -73,7 +103,7 @@ function start() { } // load app only after config is set const app = require('./src/app'); - app.start(handlers); + app.start(handlers, notificationServiceHandlers); } /** @@ -94,4 +124,11 @@ module.exports = { getAllHandlers, start, initDatabase, + addNotificationServiceHandler, + + // exposure some useful components + logger, + busService, + eventScheduler, + notificationService, }; diff --git a/migrations/v2.0.sql b/migrations/v2.0.sql new file mode 100644 index 0000000..bde0fde --- /dev/null +++ b/migrations/v2.0.sql @@ -0,0 +1,16 @@ + -- rename "deliveryMethod" column to "serviceId" + ALTER TABLE "public"."NotificationSettings" + RENAME COLUMN "deliveryMethod" TO "serviceId"; + + -- add "name" column + ALTER TABLE "public"."NotificationSettings" + ADD COLUMN "name" character varying(255); + + -- fill "name" column with the value 'enabled' + UPDATE public."NotificationSettings" + SET "name" = 'enabled'; + + -- make "name" column NOT NULL + ALTER TABLE "public"."NotificationSettings" + ALTER COLUMN "name" SET NOT NULL; + diff --git a/package-lock.json b/package-lock.json index c2347ab..2cb4986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,20 @@ "integrity": "sha1-JjNHCk6r6aR82aRf2yDtX5NAe8o=" }, "@types/body-parser": { - "version": "1.16.8", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.8.tgz", - "integrity": "sha512-BdN2PXxOFnTXFcyONPW6t0fHjz2fvRZHVMFpaS0wYr+Y8fWEaNOs4V8LEu/fpzQlMx+ahdndgTaGTwPC+J/EeA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", "requires": { - "@types/express": "4.11.1", - "@types/node": "9.6.6" + "@types/connect": "3.4.32", + "@types/node": "10.0.8" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "10.0.8" } }, "@types/events": { @@ -28,9 +36,9 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.11.1.tgz", "integrity": "sha512-ttWle8cnPA5rAelauSWeWJimtY2RsUf2aspYZs7xPHiWgOlPn6nnUfBMtrkcnjFJuIHJF4gNOdVvpLK2Zmvh6g==", "requires": { - "@types/body-parser": "1.16.8", + "@types/body-parser": "1.17.0", "@types/express-serve-static-core": "4.11.1", - "@types/serve-static": "1.13.1" + "@types/serve-static": "1.13.2" } }, "@types/express-jwt": { @@ -48,7 +56,7 @@ "integrity": "sha512-EehCl3tpuqiM8RUb+0255M8PhhSwTtLfmO7zBBdv0ay/VTd/zmrqDfQdZFsa5z/PVMbH2yCMZPXsnrImpATyIw==", "requires": { "@types/events": "1.2.0", - "@types/node": "9.6.6" + "@types/node": "10.0.8" } }, "@types/express-unless": { @@ -65,9 +73,9 @@ "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" }, "@types/lodash": { - "version": "4.14.107", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.107.tgz", - "integrity": "sha512-afvjfP2rl3yvtv2qrCRN23zIQcDinF+munMJCoHEw2BXF22QJogTlVfNPTACQ6ieDyA6VnyKT4WLuN/wK368ng==" + "version": "4.14.108", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.108.tgz", + "integrity": "sha512-WD2vUOKfBBVHxWUV9iMR9RMfpuf8HquxWeAq2yqGVL7Nc4JW2+sQama0pREMqzNI3Tutj0PyxYUJwuoxxvX+xA==" }, "@types/mime": { "version": "2.0.0", @@ -75,14 +83,14 @@ "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" }, "@types/node": { - "version": "9.6.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.6.tgz", - "integrity": "sha512-SJe0g5cZeGNDP5sD8mIX3scb+eq8LQQZ60FXiKZHipYSeEFZ5EKml+NNMiO76F74TY4PoMWlNxF/YRY40FOvZQ==" + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.8.tgz", + "integrity": "sha512-MFFKFv2X4iZy/NFl1m1E8uwE1CR96SGwJjgHma09PLtqOWoj3nqeJHMG+P/EuJGVLvC2I6MdQRQsr4TcRduIow==" }, "@types/serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-jDMH+3BQPtvqZVIcsH700Dfi8Q3MIcEx16g/VdxjoqiGR/NntekB10xdBpirMKnPe9z2C5cBmL0vte0YttOr3Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", "requires": { "@types/express-serve-static-core": "4.11.1", "@types/mime": "2.0.0" @@ -156,12 +164,12 @@ "dev": true }, "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", "requires": { - "sprintf-js": "1.0.3" + "underscore": "1.7.0", + "underscore.string": "2.4.0" } }, "array-flatten": { @@ -210,6 +218,25 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "auth0-js": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.5.1.tgz", + "integrity": "sha1-NN6msPEbXl7hOWBWEfSbHA8V27E=", + "requires": { + "base64-js": "1.3.0", + "idtoken-verifier": "1.2.0", + "js-cookie": "2.2.0", + "qs": "6.5.1", + "superagent": "3.8.3", + "url-join": "1.1.0", + "winchan": "0.2.0" + } + }, + "autolinker": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.15.3.tgz", + "integrity": "sha1-NCQX2PLzRhsUzwkIjV7fh5HcmDI=" + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -233,7 +260,7 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.6.1.tgz", "integrity": "sha1-eIuUtvY04luRvWxd9y1GdFevsAA=", "requires": { - "core-js": "2.5.5" + "core-js": "2.5.6" } }, "backoff": { @@ -254,6 +281,11 @@ "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, "base64url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", @@ -273,7 +305,7 @@ "resolved": "https://registry.npmjs.org/bin-protocol/-/bin-protocol-3.0.4.tgz", "integrity": "sha1-RlqdNQb+sOEmtStbIWDZNuFbJ/Q=", "requires": { - "lodash": "4.17.5", + "lodash": "4.17.10", "long": "3.2.0", "protocol-buffers-schema": "3.3.2" } @@ -535,9 +567,9 @@ "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=" }, "core-js": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz", - "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=" + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.6.tgz", + "integrity": "sha512-lQUVfQi0aLix2xpyjrrJEvfuYCqPc/HwmTKsC/VNf8q0zsjX7SQZtp4+oRONN5Tsur9GDETPjj+Ub2iDiGZfSQ==" }, "core-util-is": { "version": "1.0.2", @@ -571,6 +603,11 @@ } } }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -801,7 +838,7 @@ "file-entry-cache": "1.3.1", "glob": "7.1.2", "globals": "9.18.0", - "ignore": "3.3.7", + "ignore": "3.3.8", "imurmurhash": "0.1.4", "inquirer": "0.12.0", "is-my-json-valid": "2.17.2", @@ -809,7 +846,7 @@ "js-yaml": "3.11.0", "json-stable-stringify": "1.0.1", "levn": "0.3.0", - "lodash": "4.17.5", + "lodash": "4.17.10", "mkdirp": "0.5.1", "optionator": "0.8.2", "path-is-absolute": "1.0.1", @@ -1310,10 +1347,22 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" }, + "idtoken-verifier": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-1.2.0.tgz", + "integrity": "sha512-8jmmFHwdPz8L73zGNAXHHOV9yXNC+Z0TUBN5rafpoaFaLFltlIFr1JkQa3FYAETP23eSsulVw0sBiwrE8jqbUg==", + "requires": { + "base64-js": "1.3.0", + "crypto-js": "3.1.9-1", + "jsbn": "0.1.1", + "superagent": "3.8.3", + "url-join": "1.1.0" + } + }, "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", + "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==", "dev": true }, "imurmurhash": { @@ -1353,7 +1402,7 @@ "cli-cursor": "1.0.2", "cli-width": "2.2.0", "figures": "1.7.0", - "lodash": "4.17.5", + "lodash": "4.17.10", "readline2": "1.0.1", "run-async": "0.1.0", "rx-lite": "3.1.2", @@ -1473,10 +1522,10 @@ "topo": "2.0.2" } }, - "js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=" + "js-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz", + "integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=" }, "js-yaml": { "version": "3.11.0", @@ -1486,13 +1535,23 @@ "requires": { "argparse": "1.0.10", "esprima": "4.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + } } }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "json-schema": { "version": "0.2.3", @@ -1650,9 +1709,9 @@ "integrity": "sha1-/sfervF+fDoKVeHaBCgD4l2RdF0=" }, "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "lodash.cond": { "version": "4.5.2", @@ -1733,7 +1792,7 @@ "integrity": "sha1-7+ZXBsyKnMZT+A8NWm6jitlQ41I=", "requires": { "lock": "0.1.4", - "lodash": "4.17.5", + "lodash": "4.17.10", "lru-cache": "4.0.2", "very-fast-args": "1.1.0" } @@ -1803,9 +1862,9 @@ "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" }, "moment-timezone": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.16.tgz", - "integrity": "sha512-4d1l92plNNqnMkqI/7boWNVXJvwGL2WyByl1Hxp3h/ao3HZiAqaoQY+6KBkYdiN5QtNDpndq+58ozl8W4GVoNw==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.17.tgz", + "integrity": "sha512-Y/JpVEWIOA9Gho4vO15MTnW1FCmHi3ypprrkUaxsZ1TKg3uqC8q/qMBjTddkHoiwwZN3qvZSr4zJP7x9V3LpXA==", "requires": { "moment": "2.22.1" } @@ -1865,7 +1924,7 @@ "resolved": "https://registry.npmjs.org/nice-simple-logger/-/nice-simple-logger-1.0.1.tgz", "integrity": "sha1-D55khSe+e+PkmrdvqMjAmK+VG/Y=", "requires": { - "lodash": "4.17.5" + "lodash": "4.17.10" } }, "no-kafka": { @@ -1874,7 +1933,7 @@ "integrity": "sha1-jLSk8aDVDqYUXFvAZ6A1Dl5CmMc=", "requires": { "@types/bluebird": "3.5.0", - "@types/lodash": "4.14.107", + "@types/lodash": "4.14.108", "bin-protocol": "3.0.4", "bluebird": "3.5.1", "buffer-crc32": "0.2.13", @@ -1892,6 +1951,11 @@ } } }, + "node-cron": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-1.2.1.tgz", + "integrity": "sha1-jJC8XccjpWKJsHhmVatKHEy2A2g=" + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -1996,12 +2060,11 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pg": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-7.4.1.tgz", - "integrity": "sha512-Pi5qYuXro5PAD9xXx8h7bFtmHgAQEG6/SCNyi7gS3rvb/ZQYDmxKchfB0zYtiSJNWq9iXTsYsHjrM+21eBcN1A==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-7.4.3.tgz", + "integrity": "sha1-97b5P1NA7MJZavu5ShPj1rYJg0s=", "requires": { "buffer-writer": "1.0.1", - "js-string-escape": "1.0.1", "packet-reader": "0.3.1", "pg-connection-string": "0.1.3", "pg-pool": "2.0.3", @@ -2230,6 +2293,15 @@ "backoff": "2.5.0" } }, + "remarkable": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.1.tgz", + "integrity": "sha1-qspJchALZqZCpjoQIcpLrBvjv/Y=", + "requires": { + "argparse": "0.1.16", + "autolinker": "0.15.3" + } + }, "request": { "version": "2.85.0", "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", @@ -2370,9 +2442,9 @@ } }, "sequelize": { - "version": "4.37.6", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.37.6.tgz", - "integrity": "sha512-x/6099L+6+3LQWms23wng/AR6yUE3X/VhrwSTSMbgOIk2ELY3DchI/9f9Ii7LIQRPxW1BHGpwboH7kxS/froXg==", + "version": "4.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.37.7.tgz", + "integrity": "sha512-1/M1Aua2GgejZbUI3T90G3uXXjcM4gTfFC36jGsepaJh3cRK9plPmlZeKkAQWWn4bCJaJozeEtuxfyPfQUY9wg==", "requires": { "bluebird": "3.5.1", "cls-bluebird": "2.1.0", @@ -2381,16 +2453,16 @@ "dottie": "2.0.0", "generic-pool": "3.4.2", "inflection": "1.12.0", - "lodash": "4.17.5", + "lodash": "4.17.10", "moment": "2.22.1", - "moment-timezone": "0.5.16", + "moment-timezone": "0.5.17", "retry-as-promised": "2.3.2", "semver": "5.5.0", "terraformer-wkt-parser": "1.1.2", "toposort-class": "1.0.1", "uuid": "3.2.1", "validator": "9.4.1", - "wkx": "0.4.4" + "wkx": "0.4.5" }, "dependencies": { "debug": { @@ -2498,14 +2570,6 @@ "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz", "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==" }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -2517,6 +2581,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -2538,9 +2610,9 @@ "dev": true }, "superagent": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", - "integrity": "sha512-gVH4QfYHcY3P0f/BZzavLreHW3T1v7hG9B+hpMQotGQqurOvhv87GcMCd6LWySmBuf+BDR44TQd0aISjVHLeNQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", "requires": { "component-emitter": "1.2.1", "cookiejar": "2.1.1", @@ -2579,7 +2651,7 @@ "ajv": "4.11.8", "ajv-keywords": "1.5.1", "chalk": "1.1.3", - "lodash": "4.17.5", + "lodash": "4.17.10", "slice-ansi": "0.0.4", "string-width": "2.1.1" }, @@ -2628,15 +2700,16 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#eedc98867f640858fc021fd7dbaec6f7b6732051", + "version": "github:appirio-tech/tc-core-library-js#df1f5c1a5578d3d1e475bfb4a7413d9dec25525a", "requires": { + "auth0-js": "9.5.1", "axios": "0.12.0", "bunyan": "1.8.12", "config": "1.30.0", "jsonwebtoken": "7.4.3", "jwks-rsa": "1.2.1", "le_node": "1.7.1", - "lodash": "4.17.5", + "lodash": "4.17.10", "millisecond": "0.1.2" }, "dependencies": { @@ -2769,11 +2842,26 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" + }, + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "url-join": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", + "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" + }, "user-home": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", @@ -2823,6 +2911,11 @@ "resolved": "https://registry.npmjs.org/very-fast-args/-/very-fast-args-1.1.0.tgz", "integrity": "sha1-4W0dH6+KbllqJGQh/ZCneWPQs5Y=" }, + "winchan": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.0.tgz", + "integrity": "sha1-OGMCjn+XSw2hQS8oQXukJJcqvZQ=" + }, "winston": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.2.tgz", @@ -2837,11 +2930,11 @@ } }, "wkx": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.4.tgz", - "integrity": "sha512-eVVHka2jRaAp9QanKhLpxWs3AGDV0b8cijlavxBnn4ryXzq5N/3Xe3nkQsI0XMRA16RURwviCWuOCj4mXCmrxw==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.5.tgz", + "integrity": "sha512-01dloEcJZAJabLO5XdcRgqdKpmnxS0zIT02LhkdWOZX2Zs2tPM6hlZ4XG9tWaWur1Qd1OO4kJxUbe2+5BofvnA==", "requires": { - "@types/node": "9.6.6" + "@types/node": "10.0.8" } }, "wordwrap": { @@ -2869,7 +2962,7 @@ "resolved": "https://registry.npmjs.org/wrr-pool/-/wrr-pool-1.1.3.tgz", "integrity": "sha1-/a0i8uofMDY//l14HPeUl6d/8H4=", "requires": { - "lodash": "4.17.5" + "lodash": "4.17.10" } }, "xtend": { diff --git a/package.json b/package.json index 7782c6e..86a0681 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,13 @@ "lodash": "^4.17.4", "millisecond": "^0.1.2", "no-kafka": "^3.2.4", + "node-cron": "^1.2.1", "pg": "^7.3.0", + "remarkable": "^1.7.1", "sequelize": "^4.21.0", "superagent": "^3.8.0", - "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.2", - "winston": "^2.2.0", - "remarkable": "^1.7.1" + "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.3", + "winston": "^2.2.0" }, "engines": { "node": "6.x" diff --git a/src/app.js b/src/app.js index 12dc9a2..430951f 100644 --- a/src/app.js +++ b/src/app.js @@ -7,24 +7,21 @@ require('./bootstrap'); const config = require('config'); const express = require('express'); const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; -const jwt = require('jsonwebtoken'); const _ = require('lodash'); const cors = require('cors'); const bodyParser = require('body-parser'); -const { BUS_API_EVENT } = require('./constants'); const helper = require('./common/helper'); -const helperService = require('./services/helper'); const logger = require('./common/logger'); const errors = require('./common/errors'); -const service = require('./services/BusAPI'); const models = require('./models'); const Kafka = require('no-kafka'); /** * Start Kafka consumer. - * @param {Object} handlers the handlers + * @param {Object} handlers the handlers + * @param {Array} notificationServiceHandlers list of notification service handlers */ -function startKafkaConsumer(handlers) { +function startKafkaConsumer(handlers, notificationServiceHandlers) { // create group consumer const options = { groupId: config.KAFKA_GROUP_ID, connectionString: config.KAFKA_URL }; if (config.KAFKA_CLIENT_CERT && config.KAFKA_CLIENT_CERT_KEY) { @@ -53,102 +50,22 @@ function startKafkaConsumer(handlers) { const handlerAsync = Promise.promisify(handler); // use handler to create notification instances for each recipient return handlerAsync(topicName, messageJSON) - .then((notifications) => Promise.all(_.map(notifications, (notification) => + .then((notifications) => Promise.all(_.map(notifications, (notification) => { + // run other notification service handlers + notificationServiceHandlers.forEach((notificationServiceHandler) => { + notificationServiceHandler(topicName, messageJSON, notification); + }); + // save notifications - models.Notification.create({ + return models.Notification.create({ userId: notification.userId, type: notification.newType || topicName, version: notification.version || null, contents: _.extend({}, messageJSON, notification.contents), read: false, seen: false, - }) - .then(() => { - if (config.ENABLE_EMAILS) { - // if it's interesting event, create email event and send to bus api - const notificationType = notification.newType || topicName; - logger.debug(`checking ${notificationType} notification ${JSON.stringify(notification)}`); - let eventType; - - if (notificationType === BUS_API_EVENT.CONNECT.TOPIC_CREATED) { - eventType = BUS_API_EVENT.EMAIL.TOPIC_CREATED; - } else if (notificationType === BUS_API_EVENT.CONNECT.POST_CREATED) { - eventType = BUS_API_EVENT.EMAIL.POST_CREATED; - } else if (notificationType === BUS_API_EVENT.CONNECT.MENTIONED_IN_POST) { - eventType = BUS_API_EVENT.EMAIL.MENTIONED_IN_POST; - } - if (!!eventType) { - const topicId = parseInt(messageJSON.topicId, 10); - const postId = messageJSON.postId ? parseInt(messageJSON.postId, 10) : null; - - helperService.getUsersById([notification.userId]).then((users) => { - logger.debug(`got users ${JSON.stringify(users)}`); - helperService.getTopic(topicId, logger).then((connectTopic) => { - logger.debug(`got topic ${JSON.stringify(connectTopic)}`); - const user = users[0]; - let userEmail = user.email; - if (config.ENABLE_DEV_MODE === 'true') { - userEmail = config.DEV_MODE_EMAIL; - } - const recipients = [userEmail]; - const cc = []; - if (eventType === BUS_API_EVENT.EMAIL.MENTIONED_IN_POST) { - cc.push(config.MENTION_EMAIL); - } - const categories = [`${config.ENV}:${eventType}`.toLowerCase()]; - - // get jwt token then encode it with base64 - const body = { - userId: parseInt(notification.userId, 10), - topicId, - userEmail: helper.sanitizeEmail(user.email), - }; - logger.debug('body', body); - logger.debug(`body for generating token: ${JSON.stringify(body)}`); - logger.debug(`authSecret: ${config.authSecret.substring(-5)}`); - const token = jwt.sign(body, config.authSecret, { noTimestamp: true }).split('.')[2]; - logger.debug(`token: ${token}`); - - const replyTo = `${config.REPLY_EMAIL_PREFIX}+${topicId}/${token}@${config.REPLY_EMAIL_DOMAIN}`; - - const eventMessage = { - data: { - name: user.firstName + ' ' + user.lastName, - handle: user.handle, - topicTitle: connectTopic.title || '', - post: helperService.markdownToHTML(messageJSON.postContent), - date: (new Date()).toISOString(), - projectName: notification.contents.projectName, - projectId: messageJSON.projectId, - topicId, - postId, - authorHandle: notification.contents.userHandle, - }, - recipients, - replyTo, - cc, - from: { - name: notification.contents.userHandle, - email: 'topcoder@connectemail.topcoder.com',//TODO pick from config - }, - categories, - }; - // send event to bus api - return service.postEvent({ - "topic": eventType, - "originator": "tc-notifications", - "timestamp": (new Date()).toISOString(), - "mime-type": "application/json", - "payload": eventMessage, - }).then(() => { - logger.info(`sent ${eventType} event with body ${eventMessage} to bus api`); - }); - }); - }); - } - } - }) - ))) + }); + }))) // commit offset .then(() => consumer.commitOffset({ topic, partition, offset: m.offset })) .catch((err) => logger.error(err)); @@ -164,9 +81,10 @@ function startKafkaConsumer(handlers) { /** * Start the notification server. - * @param {Object} handlers the handlers + * @param {Object} handlers the handlers + * @param {Array} notificationServiceHandlers list of notification service handlers */ -function start(handlers) { +function start(handlers, notificationServiceHandlers) { const app = express(); app.set('port', config.PORT); @@ -241,7 +159,7 @@ function start(handlers) { logger.info(`Express server listening on port ${app.get('port')}`); }); - startKafkaConsumer(handlers); + startKafkaConsumer(handlers, notificationServiceHandlers); }) .catch((err) => logger.error(err)); } diff --git a/src/common/helper.js b/src/common/helper.js index a557673..c788f72 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -38,25 +38,7 @@ function autoWrapExpress(obj) { return obj; } -/** - * Helper method to clean up the provided email address for deducing the final address that matters for - * the delivery of the email i.e. removing any non standard parts in the email address e.g. getting rid - * of anything after + sign in the local part of the email. - * - * @param {String} email email address to be sanitized - * - * @returns {String} sanitized email - */ -function sanitizeEmail(email) { - if (email) { - return email.substring(0, email.indexOf('+') !== -1 ? email.indexOf('+') : email.indexOf('@')) - + email.substring(email.indexOf('@')); - } - return ''; -} - module.exports = { wrapExpress, autoWrapExpress, - sanitizeEmail, }; diff --git a/src/common/logger.js b/src/common/logger.js index 8c592e1..f96f7ab 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -12,7 +12,10 @@ const getParams = require('get-parameter-names'); const transports = []; if (!config.DISABLE_LOGGING) { - transports.push(new (winston.transports.Console)({ level: config.LOG_LEVEL })); + transports.push(new (winston.transports.Console)({ + colorize: true, + level: config.LOG_LEVEL, + })); transports.push(new (winston.transports.File)({ filename: 'log.txt', timestamp: true, diff --git a/src/models/NotificationSetting.js b/src/models/NotificationSetting.js index b3add22..39ed238 100644 --- a/src/models/NotificationSetting.js +++ b/src/models/NotificationSetting.js @@ -14,6 +14,7 @@ module.exports = (sequelize, DataTypes) => sequelize.define('NotificationSetting id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, userId: { type: DataTypes.BIGINT, allowNull: false }, topic: { type: DataTypes.STRING, allowNull: false }, - deliveryMethod: { type: DataTypes.STRING, allowNull: false }, + serviceId: { type: DataTypes.STRING, allowNull: false }, + name: { type: DataTypes.STRING, allowNull: false }, value: { type: DataTypes.STRING, allowNull: false }, }, { timestamps: false }); diff --git a/src/models/ScheduledEvents.js b/src/models/ScheduledEvents.js new file mode 100644 index 0000000..5782c88 --- /dev/null +++ b/src/models/ScheduledEvents.js @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2017 TopCoder Inc., All Rights Reserved. + */ + +/** + * the Scheduled Events schema + * + * @author TCSCODER + * @version 1.0 + */ + +module.exports = (sequelize, DataTypes) => sequelize.define('ScheduledEvents', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + schedulerId: { type: DataTypes.STRING, allowNull: false }, + data: { type: DataTypes.JSON, allowNull: false }, + // keep period as an arbitrary string so any service can define their own periods + period: { type: DataTypes.STRING, allowNull: false }, + status: { type: DataTypes.ENUM('pending', 'completed', 'failed'), allowNull: false }, + // next fields are optional, scheduler by itself doesn't rely on them + // main intention to have them is debugging production issues quickly + // though particular services may defined and use them + eventType: { type: DataTypes.STRING, allowNull: true }, + userId: { type: DataTypes.BIGINT, allowNull: true }, + reference: { type: DataTypes.STRING, allowNull: true }, + referenceId: { type: DataTypes.STRING, allowNull: true }, +}); diff --git a/src/models/ServiceSettings.js b/src/models/ServiceSettings.js new file mode 100644 index 0000000..07ba324 --- /dev/null +++ b/src/models/ServiceSettings.js @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2017 TopCoder Inc., All Rights Reserved. + */ + +/** + * the Service Settings schema + * + * @author TCSCODER + * @version 1.0 + */ + + +module.exports = (sequelize, DataTypes) => sequelize.define('ServiceSettings', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + userId: { type: DataTypes.BIGINT, allowNull: false }, + serviceId: { type: DataTypes.STRING, allowNull: false }, + name: { type: DataTypes.STRING, allowNull: false }, + value: { type: DataTypes.STRING, allowNull: true }, +}, { timestamps: false }); diff --git a/src/models/index.js b/src/models/index.js index f8d927e..e6ef09e 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -14,9 +14,13 @@ const DataTypes = require('sequelize/lib/data-types'); const Notification = require('./Notification')(sequelize, DataTypes); const NotificationSetting = require('./NotificationSetting')(sequelize, DataTypes); +const ServiceSettings = require('./ServiceSettings')(sequelize, DataTypes); +const ScheduledEvents = require('./ScheduledEvents')(sequelize, DataTypes); module.exports = { Notification, NotificationSetting, + ServiceSettings, + ScheduledEvents, init: () => sequelize.sync(), }; diff --git a/src/services/BusAPI.js b/src/services/BusAPI.js index a72d24e..49c89ce 100644 --- a/src/services/BusAPI.js +++ b/src/services/BusAPI.js @@ -1,3 +1,7 @@ +/** + * Bus API service + */ +/* global M2m */ const request = require('superagent'); const config = require('config'); const _ = require('lodash'); @@ -7,21 +11,30 @@ const _ = require('lodash'); * * @param {Object} event event * - * @return {Promise} promise resolved to post event + * @return {Promise} promise resolved to post event */ -const postEvent = (event) => request - .post(`${config.TC_API_V5_BASE_URL}/bus/events`) - .set('Content-Type', 'application/json') - .set('Authorization', `Bearer ${config.BUS_API_AUTH_TOKEN}`) - .send(event) - .then(() => '') - .catch((err) => { - const errorDetails = _.get(err, 'message'); - throw new Error( - `Failed to post event ${event}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); +const postEvent = (event) => ( + M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => ( + request + .post(`${config.TC_API_V5_BASE_URL}/bus/events`) + .set('Content-Type', 'application/json') + .set('Authorization', `Bearer ${token}`) + .send(event) + .then(() => '') + .catch((err) => { + const errorDetails = _.get(err, 'message'); + throw new Error( + `Failed to post event ${event}.` + + (errorDetails ? ' Server response: ' + errorDetails : '') + ); + }) + )) + .catch((err) => { + err.message = 'Error generating m2m token: ' + err.message; + throw err; + }) +); module.exports = { postEvent, diff --git a/src/services/EventScheduler.js b/src/services/EventScheduler.js new file mode 100644 index 0000000..0622e79 --- /dev/null +++ b/src/services/EventScheduler.js @@ -0,0 +1,126 @@ +/** + * Event scheduler + * + * Keeps scheduled events in ScheduledEvents model. + * When scheduled time comes, retrieves all due events from DB + * and passes them to the events handler to process. + */ +const _ = require('lodash'); +const cron = require('node-cron'); +const models = require('../models'); +const logger = require('../common/logger'); + +/** + * Statuses of scheduled events + * + * @constant + */ +const SCHEDULED_EVENT_STATUS = { + PENDING: 'pending', + COMPLETED: 'completed', + FAILED: 'failed', +}; + +class EventScheduler { + /** + * EventScheduler constructor + * + * @param {String} schedulerId scheduler id + * @param {Object} periods keys are period names to store in DB + * values are period definition in cron format + * @param {Function} handler function which is called when events time comes + */ + constructor(schedulerId, periods, handler) { + this.schedulerId = schedulerId; + this.periods = periods; + this.handler = handler; + + this.initSchedule(); + } + + /** + * Set status for the list of events + * + * @param {Array} events list of events + * @param {String} status events status + * + * @return {Promise} resolves to model update result + */ + static setEventsStatus(events, status) { + return models.ScheduledEvents.update({ + status, + }, { + where: { + id: _.map(events, 'id'), + }, + }); + } + + /** + * Initialize cron schedule + */ + initSchedule() { + _.forOwn(this.periods, (periodDefinition, periodName) => { + logger.verbose(`[EventScheduler] init handler '${this.schedulerId}'` + + ` period '${periodName}' (${periodDefinition}).`); + + cron.schedule(periodDefinition, () => { + logger.verbose(`[EventScheduler] run task for handler '${this.schedulerId}'` + + ` period '${periodName}' (${periodDefinition}).`); + + models.ScheduledEvents.findAll({ + where: { + schedulerId: this.schedulerId, + period: periodName, + status: [ + SCHEDULED_EVENT_STATUS.PENDING, + SCHEDULED_EVENT_STATUS.FAILED, + ], + }, + }).then((events) => this.handler(events, EventScheduler.setEventsStatus)); + }); + }); + } + + /** + * Adds events to the list of pending events + * + * @param {Object} scheduledEvent event prams to schedule + * @param {Object} scheduledEvent.data arbitrary event data + * @param {String} scheduledEvent.period event period name + * @param {Number} scheduledEvent.userId (optional) user id + * @param {String} scheduledEvent.eventType (optional) event type + * @param {String} scheduledEvent.reference (optional) target entity name (like 'topic') + * @param {String} scheduledEvent.referenceId (optional) target entity id (like ) + * + * @return {Promise} resolves to model create result + */ + addEvent(scheduledEvent) { + logger.verbose(`[EventScheduler] add event for handler '${this.schedulerId}' period '${scheduledEvent.period}'.`); + + const event = _.pick(scheduledEvent, [ + 'data', 'period', 'userId', 'eventType', 'reference', 'referenceId', + ]); + event.schedulerId = this.schedulerId; + event.status = SCHEDULED_EVENT_STATUS.PENDING; + + return models.ScheduledEvents.create(event); + } +} + +/** + * Creates EventScheduler instance + * + * @param {String} schedulerId scheduler id + * @param {Object} periods keys are period names to store in DB + * values are period definition in cron format + * @param {Function} handler function which is called when events time comes + */ +function createEventScheduler(schedulerId, periods, handler) { + return new EventScheduler(schedulerId, periods, handler); +} + +module.exports = { + SCHEDULED_EVENT_STATUS, + createEventScheduler, +}; diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js index 5c47c04..dae9625 100644 --- a/src/services/NotificationService.js +++ b/src/services/NotificationService.js @@ -17,15 +17,33 @@ const DEFAULT_LIMIT = 10; * @returns {Object} the notification settings */ function* getSettings(userId) { - const settings = yield models.NotificationSetting.findAll({ where: { userId } }); - const result = {}; - _.each(settings, (setting) => { - if (!result[setting.topic]) { - result[setting.topic] = {}; + const notificationSettings = yield models.NotificationSetting.findAll({ where: { userId } }); + const serviceSettings = yield models.ServiceSettings.findAll({ where: { userId } }); + + // format settings per notification type + const notifications = {}; + _.each(notificationSettings, (setting) => { + if (!notifications[setting.topic]) { + notifications[setting.topic] = {}; + } + if (!notifications[setting.topic][setting.serviceId]) { + notifications[setting.topic][setting.serviceId] = {}; + } + notifications[setting.topic][setting.serviceId][setting.name] = setting.value; + }); + + // format settings per service + const services = {}; + _.each(serviceSettings, (setting) => { + if (!services[setting.serviceId]) { + services[setting.serviceId] = {}; } - result[setting.topic][setting.deliveryMethod] = setting.value; + services[setting.serviceId][setting.name] = setting.value; }); - return result; + return { + notifications, + services, + }; } getSettings.schema = { @@ -37,9 +55,9 @@ getSettings.schema = { * @param {Object} entry the notification setting entry * @param {Number} userId the user id */ -function* saveSetting(entry, userId) { +function* saveNotificationSetting(entry, userId) { const setting = yield models.NotificationSetting.findOne({ where: { - userId, topic: entry.topic, deliveryMethod: entry.deliveryMethod } }); + userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name } }); if (setting) { setting.value = entry.value; yield setting.save(); @@ -47,7 +65,29 @@ function* saveSetting(entry, userId) { yield models.NotificationSetting.create({ userId, topic: entry.topic, - deliveryMethod: entry.deliveryMethod, + serviceId: entry.serviceId, + name: entry.name, + value: entry.value, + }); + } +} + +/** + * Save service setting entry. If the entry is not found, it will be created; otherwise it will be updated. + * @param {Object} entry the service setting entry + * @param {Number} userId the user id + */ +function* saveServiceSetting(entry, userId) { + const setting = yield models.ServiceSettings.findOne({ where: { + userId, serviceId: entry.serviceId, name: entry.name } }); + if (setting) { + setting.value = entry.value; + yield setting.save(); + } else { + yield models.ServiceSettings.create({ + userId, + serviceId: entry.serviceId, + name: entry.name, value: entry.value, }); } @@ -59,21 +99,71 @@ function* saveSetting(entry, userId) { * @param {Number} userId the user id */ function* updateSettings(data, userId) { - // there should be no duplicate (topic + deliveryMethod) pairs - const pairs = {}; - _.each(data, (entry) => { - const key = `${entry.topic} | ${entry.deliveryMethod}`; - if (pairs[key]) { + // convert notification settings object to the list of entries + const notifications = []; + _.forOwn(data.notifications, (notification, topic) => { + _.forOwn(notification, (serviceSettings, serviceId) => { + _.forOwn(serviceSettings, (value, name) => { + notifications.push({ + topic, + serviceId, + name, + value, + }); + }); + }); + }); + + // validation + // there should be no duplicate (topic + serviceId + name) + const triples = {}; + notifications.forEach((entry) => { + const key = `${entry.topic} | ${entry.serviceId} | ${entry.name}`; + if (triples[key]) { throw new errors.BadRequestError(`There are duplicate data for topic: ${ - entry.topic}, deliveryMethod: ${entry.deliveryMethod}`); + entry.topic}, serviceId: ${entry.serviceId}, name: ${entry.name}`); } - pairs[key] = entry; + triples[key] = entry; }); // save each entry in parallel - yield _.map(data, (entry) => saveSetting(entry, userId)); + yield _.map(notifications, (entry) => saveNotificationSetting(entry, userId)); + + // convert services settings object the the list of entries + const services = []; + _.forOwn(data.services, (service, serviceId) => { + _.forOwn(service, (value, name) => { + services.push({ + serviceId, + name, + value, + }); + }); + }); + + // validation + // there should be no duplicate (serviceId + name) + const paris = {}; + services.forEach((entry) => { + const key = `${entry.serviceId} | ${entry.name}`; + if (paris[key]) { + throw new errors.BadRequestError('There are duplicate data for' + + ` serviceId: ${entry.serviceId}, name: ${entry.name}`); + } + paris[key] = entry; + }); + + yield _.map(services, (entry) => saveServiceSetting(entry, userId)); } +updateSettings.schema = { + data: Joi.object().keys({ + notifications: Joi.object(), + services: Joi.object(), + }).required(), + userId: Joi.number().required(), +}; + /** * List notifications. * @@ -87,14 +177,17 @@ function* updateSettings(data, userId) { */ function* listNotifications(query, userId) { const settings = yield getSettings(userId); + const notificationSettings = settings.notifications; const filter = { where: { userId, }, offset: query.offset, limit: query.limit, order: [['createdAt', 'DESC']] }; - if (_.keys(settings).length > 0) { + if (_.keys(notificationSettings).length > 0) { // only filter out notifications types which were explicitly set to 'no' - so we return notification by default - const notificationTypes = _.keys(settings).filter((notificationType) => settings[notificationType].web !== 'no'); - filter.where.type = { $in: notificationTypes }; + const notifications = _.keys(notificationSettings).filter((notificationType) => + notificationSettings[notificationType].web.enabled !== 'no' + ); + filter.where.type = { $in: notifications }; } if (query.type) { filter.where.type = query.type; @@ -204,15 +297,6 @@ markAsSeen.schema = { userId: Joi.number().required(), }; -updateSettings.schema = { - data: Joi.array().min(1).items(Joi.object().keys({ - topic: Joi.string().required(), - deliveryMethod: Joi.string().required(), - value: Joi.string().required(), - })).required(), - userId: Joi.number().required(), -}; - // Exports module.exports = { listNotifications, diff --git a/src/services/helper.js b/src/services/helper.js deleted file mode 100644 index b3584af..0000000 --- a/src/services/helper.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Service to get data from TopCoder API - */ -const request = require('superagent'); -const config = require('config'); -const _ = require('lodash'); -const Remarkable = require('remarkable') - -/** - * Get users details by ids - * - * @param {Array} ids list of user ids - * - * @return {Promise} resolves to the list of user details - */ -const getUsersById = (ids) => { - const query = _.map(ids, (id) => 'id=' + id).join(' OR '); - return request - .get(`${config.TC_API_V3_BASE_URL}/users?fields=userId,email,handle,firstName,lastName&filter=${query}`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get users by id: ${ids}`); - } - const users = _.get(res, 'body.result.content'); - return users; - }).catch((err) => { - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get users by ids: ${ids}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); -}; - -/** - * Get topic details - * - * @param {String} topicId topic id - * - * @return {Promise} promise resolved to topic details - */ -const getTopic = (topicId, logger) => request - .get(`${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get topic details of topic id: ${topicId}`); - } - - return _.get(res, 'body.result.content'); - }).catch((err) => { - if (logger) { - logger.error(err, `Error while calling ${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`); - } - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get topic details of topic id: ${topicId}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); - - - -/** - * Convert markdown into raw draftjs state - * - * @param {String} markdown - markdown to convert into raw draftjs object - * @param {Object} options - optional additional data - * - * @return {Object} ContentState -**/ -const markdownToHTML = (markdown) => { - const md = new Remarkable('full', { - html: true, - linkify: true, - // typographer: true, - }) - // Replace the BBCode [u][/u] to markdown '++' for underline style - const _markdown = markdown.replace(new RegExp('\\[/?u\\]', 'g'), '++') - return md.render(_markdown, {}) // remarkable js takes markdown and makes it an array of style objects for us to easily parse -} - - -module.exports = { - getUsersById, - getTopic, - markdownToHTML, -}; diff --git a/test/token.js b/test/token.js index 25f2084..0a0c8ea 100644 --- a/test/token.js +++ b/test/token.js @@ -20,7 +20,7 @@ if (_.isNaN(userId)) { // generate JWT token const token = jwt.sign({ userId, iss: `https://api.${config.authDomain}` }, - config.authSecret, { expiresIn: '30 days' }); + config.AUTH_SECRET, { expiresIn: '30 days' }); console.info(`JWT Token: ${token}`); // eslint-disable-line no-console process.exit();