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 += '';
+ _.values(eventsByTopics).forEach((topicEvents) => {
+ emailBody += '- ';
+ emailBody += ` ${topicEvents[0].data.data.topicTitle} `;
+ emailBody += ` - ${topicEvents.length} updates`;
+ emailBody += '
';
+ });
+ 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();