diff --git a/src/notifications.js b/src/notifications.js new file mode 100644 index 000000000..762421503 --- /dev/null +++ b/src/notifications.js @@ -0,0 +1,107 @@ +'use strict'; + +exports = module.exports = { + NotificationsError: NotificationsError, + + add: add, + get: get, + ack: ack, + listPaged: listPaged +}; + +var assert = require('assert'), + DatabaseError = require('./databaseerror.js'), + debug = require('debug')('box:notifications'), + notificationdb = require('./notificationdb.js'), + util = require('util'); + +function NotificationsError(reason, errorOrMessage) { + assert.strictEqual(typeof reason, 'string'); + assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); + + Error.call(this); + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + this.reason = reason; + if (typeof errorOrMessage === 'undefined') { + this.message = reason; + } else if (typeof errorOrMessage === 'string') { + this.message = errorOrMessage; + } else { + this.message = 'Internal error'; + this.nestedError = errorOrMessage; + } +} +util.inherits(NotificationsError, Error); +NotificationsError.INTERNAL_ERROR = 'Internal Error'; +NotificationsError.NOT_FOUND = 'Not Found'; + +function au + +function add(userId, title, message, action, sendEmail, callback) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof title, 'string'); + assert.strictEqual(typeof message, 'string'); + assert.strictEqual(typeof action, 'string'); + assert.strictEqual(typeof sendEmail, 'boolean'); + assert.strictEqual(typeof userId, 'function'); + + debug('add: ', userId, title, action, 'email: ', sendEmail); + + notificationdb.add({ + userId: userId, + title: title, + message: message, + action: action + }, function (error, result) { + if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error)); + + callback(null, { id: result }); + }); +} + +function get(id, callback) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof callback, 'function'); + + debug('get: ', id); + + notificationdb.get(id, function (error, result) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND)); + if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error)); + + callback(null, result); + }); +} + +function ack(id, callback) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof callback, 'function'); + + debug('ack: ', id); + + notificationdb.update(id, { acknowledged: true }, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND)); + if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error)); + + callback(null); + }); +} + +// if acknowledged === null we return all, otherwise yes or no based on acknowledged as a boolean +function listPaged(userId, acknowledged, page, perPage, callback) { + assert.strictEqual(typeof userId, 'string'); + assert(acknowledged === null || typeof acknowledged === 'boolean'); + assert.strictEqual(typeof page, 'number'); + assert.strictEqual(typeof perPage, 'number'); + assert.strictEqual(typeof callback, 'function'); + + notificationdb.listByUserIdPaged(userId, page, perPage, function (error, result) { + if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error)); + + if (acknowledged === null) return callback(null, result); + + callback(null, result.filter(function (r) { return r.acknowledged === acknowledged; })); + }); +} diff --git a/src/routes/index.js b/src/routes/index.js index d17168859..0b85797d6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,6 +13,7 @@ exports = module.exports = { groups: require('./groups.js'), oauth2: require('./oauth2.js'), mail: require('./mail.js'), + notifications: require('./notifications.js'), profile: require('./profile.js'), provision: require('./provision.js'), services: require('./services.js'), diff --git a/src/routes/notifications.js b/src/routes/notifications.js new file mode 100644 index 000000000..98bde3308 --- /dev/null +++ b/src/routes/notifications.js @@ -0,0 +1,62 @@ +'use strict'; + +exports = module.exports = { + verifyOwnership: verifyOwnership, + get: get, + list: list, + ack: ack +}; + +let assert = require('assert'), + HttpError = require('connect-lastmile').HttpError, + HttpSuccess = require('connect-lastmile').HttpSuccess, + notifications = require('../notifications.js'), + NotificationsError = notifications.NotificationsError; + +function verifyOwnership(req, res, next) { + if (!req.params.notificationId) return next(); // skip for listing + + notifications.get(req.params.notificationId, function (error, result) { + if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification')); + if (error) return next(new HttpError(500, error)); + + if (result.userId !== req.user.id) return next(new HttpError(401, 'Unauthorized')); + + req.notification = result; + + next(); + }); +} + +function get(req, res, next) { + assert.strictEqual(typeof req.notification, 'object'); + + next(new HttpSuccess(200, { notification: req.notification })); +} + +function list(req, res, next) { + var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; + if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number')); + + var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; + if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number')); + + if (req.query.acknowledged && typeof req.query.acknowledged !== 'boolean') return next(new HttpError(400, 'acknowledged must be a boolean')); + + notifications.listPaged(req.user.id, typeof req.query.acknowledged === 'undefined' ? null : !!req.query.acknowledged, page, perPage, function (error, result) { + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, { notifications: result })); + }); +} + +function ack(req, res, next) { + assert.strictEqual(typeof req.params.notificationId, 'string'); + + notifications.ack(req.params.notificationId, function (error) { + if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(204, {})); + }); +} diff --git a/src/server.js b/src/server.js index 32134f8d2..4c3cca3c5 100644 --- a/src/server.js +++ b/src/server.js @@ -96,6 +96,7 @@ function initializeExpressSync() { var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.apps.verifyOwnership ]; var settingsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS); var mailScope = routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL); + var notificationsScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_PROFILE), routes.notifications.verifyOwnership ]; var clientsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS); var domainsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_READ); var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE); @@ -144,6 +145,11 @@ function initializeExpressSync() { router.get ('/api/v1/tasks/:taskId/logstream', cloudronScope, routes.tasks.getLogStream); router.post('/api/v1/tasks/:taskId/stop', settingsScope, routes.tasks.stopTask); + // notifications + router.get ('/api/v1/notifications', notificationsScope, routes.notifications.list); + router.get ('/api/v1/notifications/:notificationId', notificationsScope, routes.notifications.get); + router.post('/api/v1/notifications/:notificationId', notificationsScope, routes.notifications.ack); + // backups router.get ('/api/v1/backups', settingsScope, routes.backups.list); router.post('/api/v1/backups', settingsScope, routes.backups.startBackup);