diff --git a/README.md b/README.md index fb4c244..9b7ec23 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER +# TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER ## Dependencies @@ -6,7 +6,7 @@ - Heroku Toolbelt https://toolbelt.heroku.com - git - PostgreSQL 9.5 - + ## Configuration Configuration for the notification server is at `config/default.js`. @@ -125,5 +125,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/connect/events-config.js b/connect/events-config.js index 2f6f238..511ef0d 100644 --- a/connect/events-config.js +++ b/connect/events-config.js @@ -61,6 +61,10 @@ const EVENTS = [ type: 'notifications.connect.project.approved', projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER], topcoderRoles: [ROLE_CONNECT_COPILOT, ROLE_ADMINISTRATOR], + }, { + type: 'notifications.connect.project.active', + projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER], + topcoderRoles: [ROLE_ADMINISTRATOR], }, { type: 'notifications.connect.project.paused', projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER], @@ -111,6 +115,12 @@ const EVENTS = [ projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], toTopicStarter: true, toMentionedUsers: true, + }, { + type: 'notifications.connect.project.post.edited', + version: 2, + projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], + toTopicStarter: true, + toMentionedUsers: true, }, { type: 'notifications.connect.project.post.mention', }, diff --git a/docs/swagger_api.yaml b/docs/swagger_api.yaml index 1657cdc..207ddfc 100644 --- a/docs/swagger_api.yaml +++ b/docs/swagger_api.yaml @@ -70,6 +70,9 @@ paths: read: type: boolean description: read flag + seen: + type: boolean + description: seen flag contents: type: object description: the event message in JSON format @@ -152,6 +155,42 @@ paths: description: "Internal server error." schema: $ref: "#/definitions/Error" + /notifications/{id}/seen: + put: + description: + mark notification(s) as seen, id can be single id or '-' separated ids + security: + - jwt: [] + parameters: + - in: path + name: id + description: notification id + required: true + type: integer + format: int64 + responses: + 200: + description: OK, the notification(s) are marked as seen + 400: + description: "Invalid input" + schema: + $ref: "#/definitions/Error" + 401: + description: "authentication failed" + schema: + $ref: "#/definitions/Error" + 403: + description: "Action not allowed." + schema: + $ref: "#/definitions/Error" + 404: + description: "Notification is not found" + schema: + $ref: "#/definitions/Error" + 500: + description: "Internal server error." + schema: + $ref: "#/definitions/Error" /notificationsettings: get: description: diff --git a/migrations/v1.2.sql b/migrations/v1.2.sql new file mode 100644 index 0000000..5ae6633 --- /dev/null +++ b/migrations/v1.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE public."Notifications" + ADD COLUMN seen boolean; \ No newline at end of file diff --git a/package.json b/package.json index f063e42..f55a132 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "pg": "^7.3.0", "sequelize": "^4.21.0", "superagent": "^3.8.0", - "tc-core-library-js": "gondzo/tc-core-library-js.git#dev", + "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.2", "winston": "^2.2.0" }, "engines": { diff --git a/src/app.js b/src/app.js index 5a06729..798f456 100644 --- a/src/app.js +++ b/src/app.js @@ -56,6 +56,7 @@ function startKafkaConsumer(handlers) { version: notification.version || null, contents: _.extend({}, messageJSON, notification.contents), read: false, + seen: false, })))) // commit offset .then(() => consumer.commitOffset({ topic, partition, offset: m.offset })) diff --git a/src/controllers/NotificationController.js b/src/controllers/NotificationController.js index c07b9c8..4b7988b 100644 --- a/src/controllers/NotificationController.js +++ b/src/controllers/NotificationController.js @@ -34,6 +34,16 @@ function* markAllRead(req, res) { res.end(); } +/** + * Mark a notification as seen. + * @param req the request + * @param res the response + */ +function* markAsSeen(req, res) { + yield NotificationService.markAsSeen(req.params.id, req.user.userId); + res.end(); +} + /** * Get notification settings. * @param req the request @@ -58,6 +68,7 @@ module.exports = { listNotifications, markAsRead, markAllRead, + markAsSeen, getSettings, updateSettings, }; diff --git a/src/models/Notification.js b/src/models/Notification.js index 925887e..3d1a0bb 100644 --- a/src/models/Notification.js +++ b/src/models/Notification.js @@ -16,6 +16,7 @@ module.exports = (sequelize, DataTypes) => sequelize.define('Notification', { type: { type: DataTypes.STRING, allowNull: false }, contents: { type: DataTypes.JSONB, allowNull: false }, read: { type: DataTypes.BOOLEAN, allowNull: false }, + seen: { type: DataTypes.BOOLEAN, allowNull: true }, version: { type: DataTypes.SMALLINT, allowNull: true }, }, {}); diff --git a/src/routes.js b/src/routes.js index bdd7977..c8be007 100644 --- a/src/routes.js +++ b/src/routes.js @@ -19,6 +19,12 @@ module.exports = { method: 'markAllRead', }, }, + '/notifications/:id/seen': { + put: { + controller: 'NotificationController', + method: 'markAsSeen', + }, + }, '/notificationsettings': { get: { controller: 'NotificationController', diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js index 5311222..5c47c04 100644 --- a/src/services/NotificationService.js +++ b/src/services/NotificationService.js @@ -174,6 +174,36 @@ markAllRead.schema = { userId: Joi.number().required(), }; +/** + * Mark notification(s) as seen. + * @param {Number} id the notification id or '-' separated ids + * @param {Number} userId the user id + */ +function* markAsSeen(id, userId) { + const ids = _.map(id.split('-'), (str) => { + const idInt = Number(str); + if (!_.isInteger(idInt)) { + throw new errors.BadRequestError(`Notification id should be integer: ${str}`); + } + return idInt; + }); + const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, seen: { $not: true } } }); + if (!entities || entities.length === 0) { + throw new errors.NotFoundError(`Cannot find un-seen Notification where id = ${id}`); + } + _.each(entities, (entity) => { + if (Number(entity.userId) !== userId) { + throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`); + } + }); + yield models.Notification.update({ seen: true }, { where: { id: { $in: ids }, seen: { $not: true } } }); +} + +markAsSeen.schema = { + id: Joi.string().required(), + userId: Joi.number().required(), +}; + updateSettings.schema = { data: Joi.array().min(1).items(Joi.object().keys({ topic: Joi.string().required(), @@ -188,6 +218,7 @@ module.exports = { listNotifications, markAsRead, markAllRead, + markAsSeen, getSettings, updateSettings, };