diff --git a/CHANGES b/CHANGES index 021c31bcd..46c7466e5 100644 --- a/CHANGES +++ b/CHANGES @@ -2278,4 +2278,5 @@ * clone: save and restore app config * app import: restore icon, tag, label, proxy configs etc * sieve: fix redirects to not do SRS +* notifications are now system level instead of per-user diff --git a/migrations/20210528205138-notifications-drop-userId.js b/migrations/20210528205138-notifications-drop-userId.js new file mode 100644 index 000000000..1d759799e --- /dev/null +++ b/migrations/20210528205138-notifications-drop-userId.js @@ -0,0 +1,13 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE notifications DROP COLUMN userId', function (error) { + if (error) return callback(error); + + db.runSql('DELETE FROM notifications', callback); // just clear notifications table + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE notifications ADD COLUMN userId VARCHAR(128) NOT NULL', callback); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 0c096355e..928141437 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -234,7 +234,6 @@ CREATE TABLE IF NOT EXISTS tasks( CREATE TABLE IF NOT EXISTS notifications( id int NOT NULL AUTO_INCREMENT, - userId VARCHAR(128) NOT NULL, eventId VARCHAR(128), // reference to eventlog. can be null title VARCHAR(512) NOT NULL, message TEXT, diff --git a/package-lock.json b/package-lock.json index 5cb916dd3..5c5da8c28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -208,7 +208,7 @@ }, "amdefine": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "resolved": false, "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, @@ -650,7 +650,7 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, @@ -2154,7 +2154,7 @@ }, "is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "resolved": false, "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, @@ -3351,7 +3351,7 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, @@ -3440,7 +3440,7 @@ }, "parse-json": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "resolved": false, "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { @@ -4098,50 +4098,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, - "showdown": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz", - "integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==", - "requires": { - "yargs": "^14.2" - }, - "dependencies": { - "yargs": { - "version": "14.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz", - "integrity": "sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==", - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.0" - }, - "dependencies": { - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - } - } - }, - "yargs-parser": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", - "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "signal-exit": { "version": "3.0.2", "resolved": false, diff --git a/package.json b/package.json index b33103fd3..172962e54 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "s3-block-read-stream": "^0.5.0", "safetydance": "^2.0.1", "semver": "^7.3.5", - "showdown": "^1.9.1", "speakeasy": "^2.0.0", "split": "^1.0.1", "superagent": "^6.1.0", diff --git a/src/apphealthmonitor.js b/src/apphealthmonitor.js index 702275163..dd937bbef 100644 --- a/src/apphealthmonitor.js +++ b/src/apphealthmonitor.js @@ -1,6 +1,6 @@ 'use strict'; -var appdb = require('./appdb.js'), +const appdb = require('./appdb.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), diff --git a/src/eventlog.js b/src/eventlog.js index b5d6d1e68..e238e23d2 100644 --- a/src/eventlog.js +++ b/src/eventlog.js @@ -80,6 +80,7 @@ const assert = require('assert'), debug = require('debug')('box:eventlog'), eventlogdb = require('./eventlogdb.js'), notifications = require('./notifications.js'), + safe = require('safetydance'), util = require('util'), uuid = require('uuid'); @@ -93,12 +94,12 @@ function add(action, source, data, callback) { callback = callback || NOOP_CALLBACK; - eventlogdb.add(uuid.v4(), action, source, data, function (error, id) { + eventlogdb.add(uuid.v4(), action, source, data, async function (error, id) { if (error) return callback(error); callback(null, { id: id }); - notifications.onEvent(id, action, source, data, NOOP_CALLBACK); + await safe(notifications.onEvent(id, action, source, data)); }); } @@ -110,12 +111,12 @@ function upsert(action, source, data, callback) { callback = callback || NOOP_CALLBACK; - eventlogdb.upsert(uuid.v4(), action, source, data, function (error, id) { + eventlogdb.upsert(uuid.v4(), action, source, data, async function (error, id) { if (error) return callback(error); callback(null, { id: id }); - notifications.onEvent(id, action, source, data, NOOP_CALLBACK); + await safe(notifications.onEvent(id, action, source, data)); }); } diff --git a/src/mail_templates/app_updates_available.ejs b/src/mail_templates/app_updates_available.ejs deleted file mode 100644 index 7e083017e..000000000 --- a/src/mail_templates/app_updates_available.ejs +++ /dev/null @@ -1,55 +0,0 @@ -<%if (format === 'text') { %> - -Dear Cloudron Admin, - -<% for (var i = 0; i < apps.length; i++) { -%> -The app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> has an update available. - -<%= apps[i].app.manifest.title %> v<%= apps[i].updateInfo.manifest.version %> changes: -<%= apps[i].updateInfo.manifest.changelog %> - -<% } -%> - -Update now at <%= webadminUrl %> - -Powered by https://cloudron.io - -Sent at: <%= new Date().toUTCString() %> - -<% } else { %> - -
- - - -

Dear <%= cloudronName %> Admin,

- -
- -
-<% for (var i = 0; i < apps.length; i++) { -%> -

- The app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> has an update available. -

- -
<%= apps[i].app.manifest.title %> v<%= apps[i].updateInfo.manifest.version %> changes:
- <%- apps[i].changelogHTML %> - -
-<% } -%> - -

-
-

Update now
-
-

-
- -
- Powered by Cloudron.
- Sent at: <%= new Date().toUTCString() %> -
- -
- -<% } %> diff --git a/src/mail_templates/box_update_available.ejs b/src/mail_templates/box_update_available.ejs deleted file mode 100644 index 9d0c7128b..000000000 --- a/src/mail_templates/box_update_available.ejs +++ /dev/null @@ -1,45 +0,0 @@ -<%if (format === 'text') { %> - -Dear <%= cloudronName %> Admin, - -Cloudron v<%= newBoxVersion %> is now available! - -Changes: -<% for (var i = 0; i < changelog.length; i++) { %> - * <%- changelog[i] %> -<% } %> - -Powered by https://cloudron.io - -Sent at: <%= new Date().toUTCString() %> - -<% } else { %> - -
- - - -

Dear <%= cloudronName %> Admin,

- -
-

- Cloudron v<%= newBoxVersion %> is now available! -

- -
Changes:
- - -
-
- -
- Powered by Cloudron. -
- -
- -<% } %> diff --git a/src/mail_templates/box_update_error.ejs b/src/mail_templates/box_update_error.ejs deleted file mode 100644 index 8a792eb8c..000000000 --- a/src/mail_templates/box_update_error.ejs +++ /dev/null @@ -1,20 +0,0 @@ -<%if (format === 'text') { %> - -Dear Cloudron Admin, - -Cloudron update failed because of the following reason: - -------------------------------------- - -<%- message %> - -------------------------------------- - - -Powered by https://cloudron.io - -Sent at: <%= new Date().toUTCString() %> - -<% } else { %> - -<% } %> diff --git a/src/mailer.js b/src/mailer.js index fd3931fff..f5b43a5a6 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -2,8 +2,6 @@ exports = module.exports = { passwordReset, - boxUpdateAvailable, - appUpdatesAvailable, sendInvite, sendNewLoginLocation, @@ -11,14 +9,13 @@ exports = module.exports = { backupFailed, certificateRenewalError, - boxUpdateError, sendTestMail, _mailQueue: [] // accumulate mails in test mode }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('./boxerror.js'), debug = require('debug')('box:mailer'), ejs = require('ejs'), @@ -27,13 +24,12 @@ var assert = require('assert'), path = require('path'), safe = require('safetydance'), settings = require('./settings.js'), - showdown = require('showdown'), translation = require('./translation.js'), smtpTransport = require('nodemailer-smtp-transport'); -var NOOP_CALLBACK = function (error) { if (error) debug(error); }; +const NOOP_CALLBACK = function (error) { if (error) debug(error); }; -var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates'); +const MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates'); // This will collect the most common details required for notification emails function getMailConfig(callback) { @@ -215,81 +211,6 @@ function passwordReset(user) { }); } -function boxUpdateAvailable(mailTo, updateInfo, callback) { - assert.strictEqual(typeof mailTo, 'string'); - assert.strictEqual(typeof updateInfo, 'object'); - assert.strictEqual(typeof callback, 'function'); - - getMailConfig(function (error, mailConfig) { - if (error) return debug('Error getting mail details:', error); - - var converter = new showdown.Converter(); - - var templateData = { - webadminUrl: settings.dashboardOrigin(), - newBoxVersion: updateInfo.version, - changelog: updateInfo.changelog, - changelogHTML: updateInfo.changelog.map(function (e) { return converter.makeHtml(e); }), - cloudronName: mailConfig.cloudronName, - cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar' - }; - - var templateDataText = JSON.parse(JSON.stringify(templateData)); - templateDataText.format = 'text'; - - var templateDataHTML = JSON.parse(JSON.stringify(templateData)); - templateDataHTML.format = 'html'; - - var mailOptions = { - from: mailConfig.notificationFrom, - to: mailTo, - subject: `[${mailConfig.cloudronName}] Cloudron update available`, - text: render('box_update_available.ejs', templateDataText), - html: render('box_update_available.ejs', templateDataHTML) - }; - - sendMail(mailOptions, callback); - }); -} - -function appUpdatesAvailable(mailTo, apps, callback) { - assert.strictEqual(typeof mailTo, 'string'); - assert.strictEqual(typeof apps, 'object'); - assert.strictEqual(typeof callback, 'function'); - - getMailConfig(function (error, mailConfig) { - if (error) return debug('Error getting mail details:', error); - - var converter = new showdown.Converter(); - apps.forEach(function (app) { - app.changelogHTML = converter.makeHtml(app.updateInfo.manifest.changelog); - }); - - var templateData = { - webadminUrl: settings.dashboardOrigin(), - apps: apps, - cloudronName: mailConfig.cloudronName, - cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar' - }; - - var templateDataText = JSON.parse(JSON.stringify(templateData)); - templateDataText.format = 'text'; - - var templateDataHTML = JSON.parse(JSON.stringify(templateData)); - templateDataHTML.format = 'html'; - - var mailOptions = { - from: mailConfig.notificationFrom, - to: mailTo, - subject: `[${mailConfig.cloudronName}] App update available`, - text: render('app_updates_available.ejs', templateDataText), - html: render('app_updates_available.ejs', templateDataHTML) - }; - - sendMail(mailOptions, callback); - }); -} - function backupFailed(mailTo, errorMessage, logUrl) { assert.strictEqual(typeof mailTo, 'string'); @@ -326,24 +247,6 @@ function certificateRenewalError(mailTo, domain, message) { }); } -function boxUpdateError(mailTo, message) { - assert.strictEqual(typeof mailTo, 'string'); - assert.strictEqual(typeof message, 'string'); - - getMailConfig(function (error, mailConfig) { - if (error) return debug('Error getting mail details:', error); - - var mailOptions = { - from: mailConfig.notificationFrom, - to: mailTo, - subject: `[${mailConfig.cloudronName}] Cloudron update error`, - text: render('box_update_error.ejs', { message: message, format: 'text' }) - }; - - sendMail(mailOptions); - }); -} - function sendTestMail(domain, email, callback) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof email, 'string'); diff --git a/src/notificationdb.js b/src/notificationdb.js deleted file mode 100644 index dbc5ef944..000000000 --- a/src/notificationdb.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict'; - -exports = module.exports = { - get, - getByUserIdAndTitle, - add, - update, - del, - list, - - // exported for testing - _clear: clear -}; - -let assert = require('assert'), - BoxError = require('./boxerror.js'), - database = require('./database.js'); - -const NOTIFICATION_FIELDS = [ 'id', 'userId', 'eventId', 'title', 'message', 'creationTime', 'acknowledged' ]; - -function postProcess(result) { - assert.strictEqual(typeof result, 'object'); - result.id = String(result.id); - - // convert to boolean - result.acknowledged = !!result.acknowledged; -} - -function add(notification, callback) { - assert.strictEqual(typeof notification, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const query = 'INSERT INTO notifications (userId, eventId, title, message, acknowledged) VALUES (?, ?, ?, ?, ?)'; - const args = [ notification.userId, notification.eventId, notification.title, notification.message, notification.acknowledged ]; - - database.query(query, args, function (error, result) { - if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such eventlog entry')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, String(result.insertId)); - }); -} - -function getByUserIdAndTitle(userId, title, callback) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof title, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + NOTIFICATION_FIELDS + ' from notifications WHERE userId = ? AND title = ? ORDER BY creationTime LIMIT 1', [ userId, title ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found')); - - postProcess(results[0]); - - callback(null, results[0]); - }); -} - -function update(id, data, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - - let args = [ ]; - let fields = [ ]; - for (let k in data) { - fields.push(k + ' = ?'); - args.push(data[k]); - } - args.push(id); - - database.query('UPDATE notifications SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found')); - - callback(null); - }); -} - -function get(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found')); - - postProcess(result[0]); - - callback(null, result[0]); - }); -} - -function del(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM notifications WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found')); - - callback(null); - }); -} - -function list(filters, page, perPage, callback) { - assert.strictEqual(typeof filters, 'object'); - assert.strictEqual(typeof page, 'number'); - assert.strictEqual(typeof perPage, 'number'); - assert.strictEqual(typeof callback, 'function'); - - let args = []; - - let where = []; - if ('userId' in filters) { - where.push('userId=?'); - args.push(filters.userId); - } - - if ('acknowledged' in filters) { - where.push('acknowledged=?'); - args.push(filters.acknowledged); - } - - let query = `SELECT ${NOTIFICATION_FIELDS} FROM notifications`; - if (where.length) query += ' WHERE ' + where.join(' AND '); - query += ' ORDER BY creationTime DESC LIMIT ?,?'; - - args.push((page-1)*perPage); - args.push(perPage); - - database.query(query, args, function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM notifications', function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - diff --git a/src/notifications.js b/src/notifications.js index da2f34bb1..ae9fa4414 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -4,12 +4,10 @@ exports = module.exports = { get, update, list, + del, onEvent, - appUpdatesAvailable, - boxUpdateAvailable, - ALERT_BACKUP_CONFIG: 'backupConfig', ALERT_DISK_SPACE: 'diskSpace', ALERT_MAIL_STATUS: 'mailStatus', @@ -23,115 +21,111 @@ exports = module.exports = { _add: add }; -const apps = require('./apps.js'), - assert = require('assert'), - async = require('async'), +const assert = require('assert'), auditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), changelog = require('./changelog.js'), - constants = require('./constants.js'), - debug = require('debug')('box:notifications'), + database = require('./database.js'), eventlog = require('./eventlog.js'), mailer = require('./mailer.js'), - notificationdb = require('./notificationdb.js'), settings = require('./settings.js'), - users = require('./users.js'); + users = require('./users.js'), + util = require('util'); -function add(userId, eventId, title, message, callback) { - assert.strictEqual(typeof userId, 'string'); +const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'title', 'message', 'creationTime', 'acknowledged' ]; + +function postProcess(result) { + assert.strictEqual(typeof result, 'object'); + result.id = String(result.id); + + // convert to boolean + result.acknowledged = !!result.acknowledged; + return result; +} + +async function add(eventId, title, message) { assert(typeof eventId === 'string' || eventId === null); assert.strictEqual(typeof title, 'string'); assert.strictEqual(typeof message, 'string'); - assert.strictEqual(typeof callback, 'function'); - notificationdb.add({ - userId, - eventId, - title, - message, - acknowledged: false - }, function (error, result) { - if (error) return callback(error); + const query = 'INSERT INTO notifications (eventId, title, message, acknowledged) VALUES (?, ?, ?, ?)'; + const args = [ eventId, title, message, false ]; - callback(null, { id: result }); - }); + const result = await database.query(query, args); + return String(result.insertId); } -function get(id, callback) { +async function get(id) { assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - notificationdb.get(id, function (error, result) { - if (error) return callback(error); + const result = await database.query('SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE id = ?', [ id ]); + if (result.length === 0) return null; - callback(null, result); - }); + return postProcess(result[0]); } -function update(id, data, callback) { - assert.strictEqual(typeof id, 'string'); +async function getByTitle(title) { + assert.strictEqual(typeof title, 'string'); + + const results = await database.query('SELECT ' + NOTIFICATION_FIELDS + ' from notifications WHERE title = ? ORDER BY creationTime LIMIT 1', [ title ]); + if (results.length === 0) return null; + + return postProcess(results[0]); +} + +async function update(notification, data) { + assert.strictEqual(typeof notification, 'object'); assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - notificationdb.update(id, data, function (error) { - if (error) return callback(error); + let args = [ ]; + let fields = [ ]; + for (let k in data) { + fields.push(k + ' = ?'); + args.push(data[k]); + } + args.push(notification.id); - callback(null); - }); + const result = await database.query('UPDATE notifications SET ' + fields.join(', ') + ' WHERE id = ?', args); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Notification not found'); } -function list(options, page, perPage, callback) { - assert.strictEqual(typeof options, 'object'); +async function del(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query('DELETE FROM notifications WHERE id = ?', [ id ]); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Notification not found'); +} + +async function list(filters, page, perPage) { + assert.strictEqual(typeof filters, 'object'); assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof perPage, 'number'); - assert.strictEqual(typeof callback, 'function'); - notificationdb.list({ userId: options.userId, acknowledge: options.acknowledged }, page, perPage, function (error, result) { - if (error) return callback(error); + let args = []; - callback(null, result); - }); + let where = []; + if ('acknowledged' in filters) { + where.push('acknowledged=?'); + args.push(filters.acknowledged); + } + + let query = `SELECT ${NOTIFICATION_FIELDS} FROM notifications`; + if (where.length) query += ' WHERE ' + where.join(' AND '); + query += ' ORDER BY creationTime DESC LIMIT ?,?'; + + args.push((page-1)*perPage); + args.push(perPage); + + const results = await database.query(query, args); + results.forEach(postProcess); + return results; } -// Calls iterator with (admin, callback) -function forEachAdmin(options, iterator, callback) { - assert(Array.isArray(options.skip)); - assert.strictEqual(typeof iterator, 'function'); - assert.strictEqual(typeof callback, 'function'); - - users.getAdmins(function (error, result) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(); - if (error) return callback(error); - - // filter out users we want to skip (like the user who did the action or the user the action was performed on) - result = result.filter(function (r) { return options.skip.indexOf(r.id) === -1; }); - - async.each(result, iterator, callback); - }); -} - -function forEachSuperadmin(options, iterator, callback) { - assert(Array.isArray(options.skip)); - assert.strictEqual(typeof iterator, 'function'); - assert.strictEqual(typeof callback, 'function'); - - users.getSuperadmins(function (error, result) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(); - if (error) return callback(error); - - // filter out users we want to skip (like the user who did the action or the user the action was performed on) - result = result.filter(function (r) { return options.skip.indexOf(r.id) === -1; }); - - async.each(result, iterator, callback); - }); -} - -function oomEvent(eventId, app, addon, containerId, event, callback) { +async function oomEvent(eventId, app, addon, containerId /*, event*/) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof addon, 'object'); assert.strictEqual(typeof containerId, 'string'); - assert.strictEqual(typeof callback, 'function'); assert(app || addon); @@ -144,182 +138,129 @@ function oomEvent(eventId, app, addon, containerId, event, callback) { message = `The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](${settings.dashboardOrigin()}/#/services)`; } - forEachAdmin({ skip: [] }, function (admin, done) { - add(admin.id, eventId, title, message, done); - }, callback); + await add(eventId, title, message); } -function appUpdated(eventId, app, callback) { +async function appUpdated(eventId, app) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); - if (!app.appStoreId) return callback(); // skip notification of dev apps + if (!app.appStoreId) return; // skip notification of dev apps const tmp = app.manifest.description.match(/(.*)<\/upstream>/i); const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : ''; const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})` : `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`; - forEachAdmin({ skip: [] }, function (admin, done) { - add(admin.id, eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, done); - }, callback); + await add(eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`); } -function boxUpdateAvailable(updateInfo, callback) { - assert.strictEqual(typeof updateInfo, 'object'); - assert.strictEqual(typeof callback, 'function'); - - settings.getAutoupdatePattern(function (error, result) { - if (error) return callback(error); - - if (result !== constants.AUTOUPDATE_PATTERN_NEVER) return callback(); - - forEachAdmin({ skip: [] }, function (admin, done) { - mailer.boxUpdateAvailable(admin.email, updateInfo, done); - }, callback); - }); -} - -function appUpdatesAvailable(appUpdates, callback) { - assert.strictEqual(typeof appUpdates, 'object'); - assert.strictEqual(typeof callback, 'function'); - - settings.getAutoupdatePattern(function (error, result) { - if (error) return callback(error); - - // if we are auto updating, then just consider apps that cannot be auto updated - if (result !== constants.AUTOUPDATE_PATTERN_NEVER) appUpdates = appUpdates.filter(update => !apps.canAutoupdateApp(update.app, update.updateInfo)); - - if (appUpdates.length === 0) return callback(); - - forEachAdmin({ skip: [] }, function (admin, done) { - mailer.appUpdatesAvailable(admin.email, appUpdates, done); - }, callback); - }); -} - -function boxUpdated(eventId, oldVersion, newVersion, callback) { +async function boxUpdated(eventId, oldVersion, newVersion) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof oldVersion, 'string'); assert.strictEqual(typeof newVersion, 'string'); - assert.strictEqual(typeof callback, 'function'); const changes = changelog.getChanges(newVersion); const changelogMarkdown = changes.map((m) => `* ${m}\n`).join(''); - forEachAdmin({ skip: [] }, function (admin, done) { - add(admin.id, eventId, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, done); - }, callback); + await add(eventId, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`); } -function boxUpdateError(eventId, errorMessage, callback) { +async function boxUpdateError(eventId, errorMessage) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof errorMessage, 'string'); - assert.strictEqual(typeof callback, 'function'); - forEachAdmin({ skip: [] }, function (admin, done) { - mailer.boxUpdateError(admin.email, errorMessage); - add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, done); - }, callback); + await add(eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`); } -function certificateRenewalError(eventId, vhost, errorMessage, callback) { +async function certificateRenewalError(eventId, vhost, errorMessage) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof vhost, 'string'); assert.strictEqual(typeof errorMessage, 'string'); - assert.strictEqual(typeof callback, 'function'); - forEachAdmin({ skip: [] }, function (admin, callback) { + const getAdmins = util.callbackify(users.getAdmins); + + const admins = await getAdmins(); + + for (const admin of admins) { mailer.certificateRenewalError(admin.email, vhost, errorMessage); - add(admin.id, eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours`, callback); - }, callback); + } + + await add(eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours`); } -function backupFailed(eventId, taskId, errorMessage, callback) { +async function backupFailed(eventId, taskId, errorMessage) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof taskId, 'string'); assert.strictEqual(typeof errorMessage, 'string'); - assert.strictEqual(typeof callback, 'function'); - forEachSuperadmin({ skip: [] }, function (admin, callback) { - mailer.backupFailed(admin.email, errorMessage, `${settings.dashboardOrigin()}/logs.html?taskId=${taskId}`); - add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`, callback); - }, callback); + const getSuperAdmins = util.callbackify(users.getSuperAdmins); + + const superAdmins = await getSuperAdmins(); + + for (const superAdmin of superAdmins) { + mailer.backupFailed(superAdmin.email, errorMessage, `${settings.dashboardOrigin()}/logs.html?taskId=${taskId}`); + } + + await add(eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`); } -function alert(id, title, message, callback) { +async function alert(id, title, message) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof title, 'string'); assert.strictEqual(typeof message, 'string'); - assert.strictEqual(typeof callback, 'function'); const acknowledged = !message; - forEachAdmin({ skip: [] }, function (admin, callback) { - const data = { - userId: admin.id, + const result = await getByTitle(title); + if (!result && acknowledged) return; // do not add acked alerts + + if (!result) { + await add(null /* eventId */, title, message); + } else { + await update(result, { eventId: null, title, message, acknowledged, creationTime: new Date() - }; - - notificationdb.getByUserIdAndTitle(admin.id, title, function (error, result) { - if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); - - if (!result && acknowledged) return callback(); // do not add acked alerts - - let updateFunc = !result ? notificationdb.add.bind(null, data) : notificationdb.update.bind(null, result.id, data); - - updateFunc(function (error) { - if (error) return callback(error); - - callback(null); - }); }); - }, function (error) { - if (error) debug('alert: error notifying', error); - - callback(); - }); + } } -function onEvent(id, action, source, data, callback) { +async function onEvent(id, action, source, data) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof action, 'string'); assert.strictEqual(typeof source, 'object'); assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); // external ldap syncer does not generate notifications - FIXME username might be an issue here - if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return callback(); + if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return; switch (action) { case eventlog.ACTION_APP_OOM: - return oomEvent(id, data.app, data.addon, data.containerId, data.event, callback); + return await oomEvent(id, data.app, data.addon, data.containerId, data.event); case eventlog.ACTION_APP_UPDATE_FINISH: - return appUpdated(id, data.app, callback); + return await appUpdated(id, data.app); case eventlog.ACTION_CERTIFICATE_RENEWAL: case eventlog.ACTION_CERTIFICATE_NEW: - if (!data.errorMessage) return callback(); - return certificateRenewalError(id, data.domain, data.errorMessage, callback); + if (!data.errorMessage) return; + return await certificateRenewalError(id, data.domain, data.errorMessage); case eventlog.ACTION_BACKUP_FINISH: - if (!data.errorMessage) return callback(); - if (source.username !== auditSource.CRON.username && !data.timedOut) return callback(); // manual stop by user + if (!data.errorMessage) return; + if (source.username !== auditSource.CRON.username && !data.timedOut) return; // manual stop by user - return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups or timedout + return await backupFailed(id, data.taskId, data.errorMessage); // only notify for automated backups or timedout case eventlog.ACTION_UPDATE_FINISH: - if (!data.errorMessage) return boxUpdated(id, data.oldVersion, data.newVersion, callback); - if (data.timedOut) return boxUpdateError(id, data.errorMessage, callback); - return callback(); + if (!data.errorMessage) return await boxUpdated(id, data.oldVersion, data.newVersion); + if (data.timedOut) return await boxUpdateError(id, data.errorMessage); + return; default: - return callback(); + return; } } diff --git a/src/routes/notifications.js b/src/routes/notifications.js index 4c83be849..612b7f06c 100644 --- a/src/routes/notifications.js +++ b/src/routes/notifications.js @@ -1,7 +1,7 @@ 'use strict'; exports = module.exports = { - verifyOwnership, + load, get, list, update @@ -11,29 +11,27 @@ let assert = require('assert'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, - notifications = require('../notifications.js'); + notifications = require('../notifications.js'), + safe = require('safetydance'); -function verifyOwnership(req, res, next) { - if (!req.params.notificationId) return next(); // skip for listing +async function load(req, res, next) { + assert.strictEqual(typeof req.params.notificationId, 'string'); - notifications.get(req.params.notificationId, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(notifications.get(req.params.notificationId)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'Notification not found')); - if (result.userId !== req.user.id) return next(new HttpError(403, 'User is not owner')); - - req.notification = result; - - next(); - }); + req.resource = result; + next(); } function get(req, res, next) { - assert.strictEqual(typeof req.notification, 'object'); + assert.strictEqual(typeof req.resource, 'object'); - next(new HttpSuccess(200, { notification: req.notification })); + next(new HttpSuccess(200, req.resource)); } -function list(req, res, next) { +async function list(req, res, next) { const 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')); @@ -42,24 +40,20 @@ function list(req, res, next) { if (req.query.acknowledged && !(req.query.acknowledged === 'true' || req.query.acknowledged === 'false')) return next(new HttpError(400, 'acknowledged must be a true or false')); - const acknowledged = req.query.acknowledged ? req.query.acknowledged === 'true' : null; + const acknowledged = req.query.acknowledged ? req.query.acknowledged === 'true' : false; - notifications.list({ userId: req.user.id, acknowledged }, page, perPage, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, { notifications: result })); - }); + const [error, result] = await safe(notifications.list({ userId: req.user.id, acknowledged }, page, perPage)); + if (error) return next(BoxError.toHttpError(error)); + next(new HttpSuccess(200, { notifications: result })); } -function update(req, res, next) { - assert.strictEqual(typeof req.params.notificationId, 'string'); +async function update(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.acknowledged !== 'boolean') return next(new HttpError(400, 'acknowledged must be a booliean')); - notifications.update(req.params.notificationId, { acknowledged: req.body.acknowledged }, function (error) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(204, {})); - }); + const [error] = await safe(notifications.update(req.resource, { acknowledged: req.body.acknowledged })); + if (error) return next(BoxError.toHttpError(error)); + next(new HttpSuccess(204, {})); } diff --git a/src/server.js b/src/server.js index e42e216ef..5cbafa134 100644 --- a/src/server.js +++ b/src/server.js @@ -132,9 +132,9 @@ function initializeExpressSync() { router.post('/api/v1/tasks/:taskId/stop', json, token, authorizeAdmin, routes.tasks.stopTask); // notification routes (these are server level) - router.get ('/api/v1/notifications', token, routes.notifications.verifyOwnership, routes.notifications.list); - router.get ('/api/v1/notifications/:notificationId', token, routes.notifications.verifyOwnership, routes.notifications.get); - router.post('/api/v1/notifications/:notificationId', json, token, routes.notifications.verifyOwnership, routes.notifications.update); + router.get ('/api/v1/notifications', token, authorizeAdmin, routes.notifications.list); + router.get ('/api/v1/notifications/:notificationId', token, authorizeAdmin, routes.notifications.load, routes.notifications.get); + router.post('/api/v1/notifications/:notificationId', json, token, authorizeAdmin, routes.notifications.load, routes.notifications.update); // backup routes router.get ('/api/v1/backups', token, authorizeAdmin, routes.backups.list); diff --git a/src/test/database-test.js b/src/test/database-test.js index f0db826f3..27db578f7 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -20,7 +20,6 @@ const appdb = require('../appdb.js'), hat = require('../hat.js'), mailboxdb = require('../mailboxdb.js'), maildb = require('../maildb.js'), - notificationdb = require('../notificationdb.js'), reverseProxy = require('../reverseproxy.js'), settingsdb = require('../settingsdb.js'), taskdb = require('../taskdb.js'), @@ -120,190 +119,6 @@ describe('database', function () { ], done); }); - describe('notifications', function () { - var EVENT_0 = { - id: 'event_0', - action: 'action', - source: {}, - data: {} - }; - - var EVENT_1 = { - id: 'event_1', - action: 'action', - source: {}, - data: {} - }; - - var EVENT_2 = { - id: 'event_2', - action: 'action', - source: {}, - data: {} - }; - - var NOTIFICATION_0 = { - userId: USER_0.id, - eventId: EVENT_0.id, - title: 'title z', // titles are this way for ordering - message: 'some message there', - }; - - var NOTIFICATION_1 = { - userId: USER_0.id, - eventId: EVENT_1.id, - title: 'title y', - message: 'some message there', - }; - - var NOTIFICATION_2 = { - userId: USER_1.id, - eventId: EVENT_2.id, - title: 'title x', - message: 'some message there', - }; - - var NOTIFICATION_3 = { - userId: USER_0.id, - eventId: null, - title: 'title w', - message: 'some message there', - }; - - before(function (done) { - async.series([ - userdb.add.bind(null, USER_0.id, USER_0), - userdb.add.bind(null, USER_1.id, USER_1), - eventlogdb.add.bind(null, EVENT_0.id, EVENT_0.action, EVENT_0.source, EVENT_0.data), - eventlogdb.add.bind(null, EVENT_1.id, EVENT_1.action, EVENT_1.source, EVENT_1.data), - eventlogdb.add.bind(null, EVENT_2.id, EVENT_2.action, EVENT_2.source, EVENT_2.data), - ], done); - }); - - after(function (done) { - database._clear(done); - }); - - it('can add notification', function (done) { - notificationdb.add(NOTIFICATION_0, function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.a('string'); - NOTIFICATION_0.id = result; - done(); - }); - }); - - it('can add second notification', function (done) { - notificationdb.add(NOTIFICATION_1, function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.a('string'); - NOTIFICATION_1.id = result; - done(); - }); - }); - - it('can add third notification for another user', function (done) { - notificationdb.add(NOTIFICATION_2, function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.a('string'); - NOTIFICATION_2.id = result; - done(); - }); - }); - - it('can get by id', function (done) { - notificationdb.get(NOTIFICATION_0.id, function (error, result) { - expect(error).to.equal(null); - expect(result.id).to.equal(NOTIFICATION_0.id); - expect(result.title).to.equal(NOTIFICATION_0.title); - expect(result.message).to.equal(NOTIFICATION_0.message); - expect(result.acknowledged).to.equal(false); - done(); - }); - }); - - it('cannot get by non-existing id', function (done) { - notificationdb.get('nopenothere', function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - - it('can list by user', function (done) { - notificationdb.list({ userId: USER_0.id }, 1, 100, function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.an('array'); - expect(result.length).to.equal(2); - expect(result[0].id).to.equal(NOTIFICATION_0.id); - expect(result[0].title).to.equal(NOTIFICATION_0.title); - expect(result[0].message).to.equal(NOTIFICATION_0.message); - expect(result[0].acknowledged).to.equal(false); - done(); - }); - }); - - it('cannot update non-existing notification', function (done) { - notificationdb.update('isnotthere', { acknowledged: true }, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - done(); - }); - }); - - it('update succeeds', function (done) { - notificationdb.update(NOTIFICATION_1.id, { acknowledged: true }, function (error) { - expect(error).to.equal(null); - - notificationdb.get(NOTIFICATION_1.id, function (error, result) { - expect(error).to.equal(null); - expect(result.id).to.equal(NOTIFICATION_1.id); - expect(result.title).to.equal(NOTIFICATION_1.title); - expect(result.message).to.equal(NOTIFICATION_1.message); - expect(result.acknowledged).to.equal(true); - - done(); - }); - }); - }); - - it('deletion succeeds', function (done) { - notificationdb.del(NOTIFICATION_0.id, function (error) { - expect(error).to.equal(null); - - notificationdb.get(NOTIFICATION_0.id, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - - done(); - }); - }); - }); - - it('deletion for non-existing notification fails', function (done) { - notificationdb.del('doesnotexts', function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - - done(); - }); - }); - - it('can add notification without eventId', function (done) { - notificationdb.add(NOTIFICATION_3, function (error, result) { - expect(error).to.equal(null); - expect(result).to.be.a('string'); - - // stash for further use - NOTIFICATION_3.id = result; - - done(); - }); - }); - }); - describe('domains', function () { before(function (done) { userdb.add(USER_0.id, USER_0, done); diff --git a/src/test/notifications-test.js b/src/test/notifications-test.js index d11587234..20322b305 100644 --- a/src/test/notifications-test.js +++ b/src/test/notifications-test.js @@ -6,51 +6,24 @@ 'use strict'; -var async = require('async'), +const async = require('async'), BoxError = require('../boxerror.js'), database = require('../database.js'), - users = require('../users.js'), - userdb = require('../userdb.js'), - eventlogdb = require('../eventlogdb.js'), + expect = require('expect.js'), notifications = require('../notifications.js'), - expect = require('expect.js'); + safe = require('safetydance'); -// owner -var USER_0 = { - username: 'username0', - password: 'Username0pass?1234', - email: 'user0@email.com', - fallbackEmail: 'user0fallback@email.com', - displayName: 'User 0', - role: 'owner' -}; - -var EVENT_0 = { +const EVENT_0 = { id: 'event_0', - action: '', + action: 'action', source: {}, data: {} }; -var AUDIT_SOURCE = { - ip: '1.2.3.4' -}; - function setup(done) { async.series([ database.initialize, database._clear, - users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), - function (callback) { - userdb.getByUsername(USER_0.username, function (error, result) { - if (error) return callback(error); - - USER_0.id = result.id; - - callback(); - }); - }, - eventlogdb.add.bind(null, EVENT_0.id, EVENT_0.action, EVENT_0.source, EVENT_0.data), ], done); } @@ -65,114 +38,59 @@ describe('Notifications', function () { before(setup); after(cleanup); - var notificationId; + let notificationIds = []; - it('add succeeds', function (done) { - notifications._add(USER_0.id, EVENT_0.id, 'title', 'message text', function (error, result) { - expect(error).to.eql(null); - expect(result.id).to.be.ok(); - - notificationId = result.id; - - done(); - }); + it('can add notifications', async function () { + for (let i = 0; i < 3; i++) { + const [error, id] = await safe(notifications._add(EVENT_0.id, `title ${i}`, `message ${i}`)); + expect(error).to.equal(null); + expect(id).to.be.a('string'); + notificationIds.push(id); + } }); - it('get succeeds', function (done) { - notifications.get(notificationId, function (error, result) { - expect(error).to.eql(null); - expect(result.id).to.equal(notificationId); - expect(result.title).to.equal('title'); - expect(result.message).to.equal('message text'); - expect(result.acknowledged).to.equal(false); - expect(result.creationTime).to.be.a(Date); - - done(); - }); + it('can get by id', async function () { + const [error, result] = await safe(notifications.get(notificationIds[0])); + expect(error).to.be(null); + expect(result.title).to.be('title 0'); + expect(result.message).to.be('message 0'); + expect(result.acknowledged).to.be(false); }); - it('get of unknown id fails', function (done) { - notifications.get('notfoundid', function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - - done(); - }); + it('cannot get non-existent id', async function () { + const result = await notifications.get('random'); + expect(result).to.be(null); }); - it('ack succeeds', function (done) { - notifications.ack(notificationId, function (error) { - expect(error).to.eql(null); - - notifications.get(notificationId, function (error, result) { - expect(error).to.eql(null); - expect(result.acknowledged).to.equal(true); - - done(); - }); - }); + it('can list notifications', async function () { + const result = await notifications.list({}, 1, 10); + expect(result.length).to.be(3); + expect(result[0].title).to.be('title 0'); + expect(result[1].title).to.be('title 1'); + expect(result[2].title).to.be('title 2'); }); - it('ack succeeds twice', function (done) { - notifications.ack(notificationId, function (error) { - expect(error).to.eql(null); + it('can update notification', async function () { + await notifications.update({ id: notificationIds[0] }, { title: 'updated title 0', message: 'updated message 0', acknowledged: true }); - notifications.get(notificationId, function (error, result) { - expect(error).to.eql(null); - expect(result.acknowledged).to.equal(true); - - done(); - }); - }); + const result = await notifications.get(notificationIds[0]); + expect(result.title).to.be('updated title 0'); + expect(result.message).to.be('updated message 0'); + expect(result.acknowledged).to.be(true); }); - it('ack fails for nonexisting id', function (done) { - notifications.ack('id does not exist', function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); + it('cannot update non-existent notification', async function () { + const [error] = await safe(notifications.update({ id: 'random' }, { title: 'updated title 0', message: 'updated message 0', acknowledged: true })); + expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); }); - it('getAllPaged succeeds', function (done) { - notifications.getAllPaged(USER_0.id, null, 1, 1, function (error, results) { - expect(error).to.eql(null); - expect(results).to.be.an(Array); - expect(results.length).to.be(1); - - expect(results[0].id).to.be(notificationId); - expect(results[0].title).to.equal('title'); - expect(results[0].message).to.equal('message text'); - expect(results[0].acknowledged).to.equal(true); - expect(results[0].creationTime).to.be.a(Date); - - done(); - }); + it('can delete', async function () { + await notifications.del(notificationIds[0]); }); - it('getAllPaged succeeds for second page (takes 5 seconds to add)', function (done) { - async.timesSeries(5, function (n, callback) { - // timeout is for database TIMESTAMP resolution - setTimeout(function () { - notifications._add(USER_0.id, EVENT_0.id, 'title' + n, 'some message', callback); - }, 1000); - }, function (error) { - expect(error).to.eql(null); - - notifications.getAllPaged(USER_0.id, null /* ack */, 2, 3, function (error, results) { - expect(error).to.eql(null); - expect(results).to.be.an(Array); - expect(results.length).to.be(3); - - expect(results[0].title).to.equal('title1'); - expect(results[1].title).to.equal('title0'); - // the previous tests already add one notification with 'title' - expect(results[2].title).to.equal('title'); - - done(); - }); - }); + it('cannot delete non-existent notification', async function () { + const [error] = await safe(notifications.del('random')); + expect(error.reason).to.be(BoxError.NOT_FOUND); }); }); diff --git a/src/updatechecker.js b/src/updatechecker.js index d87e087e7..3940036b1 100644 --- a/src/updatechecker.js +++ b/src/updatechecker.js @@ -8,7 +8,7 @@ exports = module.exports = { _checkAppUpdates: checkAppUpdates }; -var apps = require('./apps.js'), +const apps = require('./apps.js'), appstore = require('./appstore.js'), assert = require('assert'), async = require('async'), @@ -40,8 +40,6 @@ function checkAppUpdates(options, callback) { let state = getUpdateInfo(); let newState = { }; // create new state so that old app ids are removed - var pendingNotifications = []; - apps.getAll(function (error, result) { if (error) return callback(error); @@ -58,14 +56,6 @@ function checkAppUpdates(options, callback) { newState[app.id] = updateInfo; - if (safe.query(state[app.id], 'manifest.version') === updateInfo.manifest.version) { - debug(`checkAppUpdates: Skipping app update notification of ${app.id} since user was already notified of ${updateInfo.manifest.version}`); - return iteratorDone(); - } - - debug(`checkAppUpdates: ${app.id} can be updated to ${updateInfo.manifest.id}@${updateInfo.manifest.version}`); - - pendingNotifications.push({ app, updateInfo }); iteratorDone(); }); }, function () { @@ -73,7 +63,7 @@ function checkAppUpdates(options, callback) { setUpdateInfo(newState); - notifications.appUpdatesAvailable(pendingNotifications, callback); + callback(); }); }); } @@ -115,7 +105,7 @@ function checkBoxUpdates(options, callback) { state.box = updateInfo; setUpdateInfo(state); - notifications.boxUpdateAvailable(updateInfo, callback); + callback(); }); }); }