mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
5b77f052d9
closes #5071 - Remove hardcoded notification in admin controller - NOTE: update check notifications are no longer blocking the admin rendering - this is one of the most import changes - we remove the hardcoded release message - we also remove adding a notification manually in here, because this will work differently from now on -> you receive a notification (release or custom) in the update check module and this module adds the notification as is to our database - Change default core settings keys - remove displayUpdateNotification -> this was used to store the release version number send from the UCS -> based on this value, Ghost creates a notification container with self defined values -> not needed anymore - rename seenNotifications to notifications -> the new notifications key will hold both 1. the notification from the USC 2. the information about if a notification was seen or not - this key hold only one release notification - and n custom notifications - Update Check Module: Request to the USC depends on the privacy configuration - useUpdateCheck: true -> does a checkin in the USC (exposes data) - useUpdateCheck: false -> does only a GET query to the USC (does not expose any data) - make the request handling dynamic, so it depends on the flag - add an extra logic to be able to define a custom USC endpoint (helpful for testing) - add an extra logic to be able to force the request to the service (helpful for testing) - Update check module: re-work condition when a check should happen - only if the env is not correct - remove deprecated config.updateCheck - remove isPrivacyDisabled check (handled differently now, explained in last commit) - Update check module: remove `showUpdateNotification` and readability - showUpdateNotification was used in the admin controller to fetch the latest release version number from the db - no need to check against semver in general, the USC takes care of that (no need to double check) - improve readability of `nextUpdateCheck` condition - Update check module: refactor `updateCheckResponse` - remove db call to displayUpdateNotification, not used anymore - support receiving multiple custom notifications - support custom notification groups - the default group is `all` - this will always be consumed - groups can be extended via config e.g. `notificationGroups: ['migration']` - Update check module: refactor createCustomNotification helper - get rid of taking over notification duplication handling (this is not the task of the update check module) - ensure we have good fallback values for non present attributes in a notification - get rid of semver check (happens in the USC) - could be reconsidered later if LTS is gone - Refactor notification API - reason: get rid of in process notification store -> this was an object hold in process -> everything get's lost after restart -> not helpful anymore, because imagine the following case -> you get a notification -> you store it in process -> you mark this notification as seen -> you restart Ghost, you will receive the same notification on the next check again -> because we are no longer have a separate seen notifications object - use database settings key `notification` instead - refactor all api endpoints to support reading and storing into the `notifications` object - most important: notification deletion happens via a `seen` property (the notification get's physically deleted 3 month automatically) -> we have to remember a seen property, because otherwise you don't know which notification was already received/seen - Add listener to remove seen notifications automatically after 3 month - i just decided for 3 month (we can decrease?) - at the end it doesn't really matter, as long as the windows is not tooooo short - listen on updates for the notifications settings - check if notification was seen and is older than 3 month - ignore release notification - Updated our privacy document - Updated docs.ghost.org for privacy config behaviour - contains a migration script to remove old settings keys
294 lines
10 KiB
JavaScript
294 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
// # Notifications API
|
|
// RESTful API for creating notifications
|
|
|
|
const Promise = require('bluebird'),
|
|
_ = require('lodash'),
|
|
moment = require('moment'),
|
|
ObjectId = require('bson-objectid'),
|
|
pipeline = require('../lib/promise/pipeline'),
|
|
permissions = require('../services/permissions'),
|
|
localUtils = require('./utils'),
|
|
common = require('../lib/common'),
|
|
SettingsAPI = require('./settings'),
|
|
internalContext = {context: {internal: true}},
|
|
canThis = permissions.canThis;
|
|
|
|
let notifications,
|
|
_private = {};
|
|
|
|
_private.fetchAllNotifications = function fetchAllNotifications() {
|
|
let allNotifications;
|
|
|
|
return SettingsAPI.read(_.merge({key: 'notifications'}, internalContext))
|
|
.then(function (response) {
|
|
allNotifications = JSON.parse(response.settings[0].value || []);
|
|
|
|
_.each(allNotifications, function (notification) {
|
|
notification.addedAt = moment(notification.addedAt).toDate();
|
|
});
|
|
|
|
return allNotifications;
|
|
});
|
|
};
|
|
|
|
_private.publicResponse = function publicResponse(notificationsToReturn) {
|
|
_.each(notificationsToReturn, function (notification) {
|
|
delete notification.seen;
|
|
delete notification.addedAt;
|
|
});
|
|
|
|
return {
|
|
notifications: notificationsToReturn
|
|
};
|
|
};
|
|
|
|
/**
|
|
* ## Notification API Methods
|
|
*
|
|
* **See:** [API Methods](constants.js.html#api%20methods)
|
|
*/
|
|
notifications = {
|
|
|
|
/**
|
|
* ### Browse
|
|
* Fetch all notifications
|
|
* @returns {Promise(Notifications)}
|
|
*/
|
|
browse: function browse(options) {
|
|
return canThis(options.context).browse.notification().then(function () {
|
|
return _private.fetchAllNotifications()
|
|
.then(function (allNotifications) {
|
|
allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
|
|
|
|
allNotifications = allNotifications.filter(function (notification) {
|
|
return notification.seen !== true;
|
|
});
|
|
|
|
return _private.publicResponse(allNotifications);
|
|
});
|
|
}, function () {
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
message: common.i18n.t('errors.api.notifications.noPermissionToBrowseNotif')
|
|
}));
|
|
});
|
|
},
|
|
|
|
/**
|
|
* ### Add
|
|
*
|
|
*
|
|
* **takes:** a notification object of the form
|
|
*
|
|
* If notification message already exists, we return the existing notification object.
|
|
*
|
|
* ```
|
|
* msg = { notifications: [{
|
|
* status: 'alert', // A String. Can be 'alert' or 'notification'
|
|
* type: 'error', // A String. Can be 'error', 'success', 'warn' or 'info'
|
|
* message: 'This is an error', // A string. Should fit in one line.
|
|
* location: '', // A String. Should be unique key to the notification, usually takes the form of "noun.verb.message", eg: "user.invite.already-invited"
|
|
* dismissible: true // A Boolean. Whether the notification is dismissible or not.
|
|
* custom: true // A Boolean. Whether the notification is a custom message intended for particular Ghost versions.
|
|
* }] };
|
|
* ```
|
|
*/
|
|
add: function add(object, options) {
|
|
var tasks;
|
|
|
|
/**
|
|
* ### Handle Permissions
|
|
* We need to be an authorised user to perform this action
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
function handlePermissions(options) {
|
|
if (permissions.parseContext(options.context).internal) {
|
|
return Promise.resolve(options);
|
|
}
|
|
|
|
return canThis(options.context).add.notification().then(function () {
|
|
return options;
|
|
}, function () {
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
message: common.i18n.t('errors.api.notifications.noPermissionToAddNotif')
|
|
}));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ### Save Notifications
|
|
* Save the notifications
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
function saveNotifications(options) {
|
|
let defaults = {
|
|
dismissible: true,
|
|
location: 'bottom',
|
|
status: 'alert',
|
|
id: ObjectId.generate()
|
|
},
|
|
overrides = {
|
|
seen: false,
|
|
addedAt: moment().toDate()
|
|
},
|
|
notificationsToCheck = options.data.notifications,
|
|
addedNotifications = [];
|
|
|
|
return _private.fetchAllNotifications()
|
|
.then(function (allNotifications) {
|
|
_.each(notificationsToCheck, function (notification) {
|
|
let isDuplicate = _.find(allNotifications, {id: notification.id});
|
|
|
|
if (!isDuplicate) {
|
|
addedNotifications.push(_.merge({}, defaults, notification, overrides));
|
|
}
|
|
});
|
|
|
|
let hasReleaseNotification = _.find(notificationsToCheck, {custom: false});
|
|
|
|
// CASE: remove any existing release notifications if a new release notification comes in
|
|
if (hasReleaseNotification) {
|
|
_.remove(allNotifications, function (el) {
|
|
return !el.custom;
|
|
});
|
|
}
|
|
|
|
// CASE: nothing to add, skip
|
|
if (!addedNotifications.length) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
let addedReleaseNotifications = _.filter(addedNotifications, {custom: false});
|
|
|
|
// CASE: only latest release notification
|
|
if (addedReleaseNotifications.length > 1) {
|
|
addedNotifications = _.filter(addedNotifications, {custom: true});
|
|
addedNotifications.push(_.orderBy(addedReleaseNotifications, 'created_at', 'desc')[0]);
|
|
}
|
|
|
|
return SettingsAPI.edit({
|
|
settings: [{
|
|
key: 'notifications',
|
|
value: allNotifications.concat(addedNotifications)
|
|
}]
|
|
}, internalContext);
|
|
})
|
|
.then(function () {
|
|
return _private.publicResponse(addedNotifications);
|
|
});
|
|
}
|
|
|
|
tasks = [
|
|
localUtils.validate('notifications'),
|
|
handlePermissions,
|
|
saveNotifications
|
|
];
|
|
|
|
return pipeline(tasks, object, options);
|
|
},
|
|
|
|
/**
|
|
* ### Destroy
|
|
* Remove a specific notification
|
|
*
|
|
* @param {{id (required), context}} options
|
|
* @returns {Promise}
|
|
*/
|
|
destroy: function destroy(options) {
|
|
let tasks;
|
|
|
|
/**
|
|
* ### Handle Permissions
|
|
* We need to be an authorised user to perform this action
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
function handlePermissions(options) {
|
|
return canThis(options.context).destroy.notification().then(function () {
|
|
return options;
|
|
}, function () {
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
message: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')
|
|
}));
|
|
});
|
|
}
|
|
|
|
function destroyNotification(options) {
|
|
return _private.fetchAllNotifications()
|
|
.then(function (allNotifications) {
|
|
let notificationToMarkAsSeen = _.find(allNotifications, {id: options.id}),
|
|
notificationToMarkAsSeenIndex = _.findIndex(allNotifications, {id: options.id});
|
|
|
|
if (notificationToMarkAsSeenIndex > -1 && !notificationToMarkAsSeen.dismissible) {
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')
|
|
}));
|
|
}
|
|
|
|
if (notificationToMarkAsSeenIndex < 0) {
|
|
return Promise.reject(new common.errors.NotFoundError({
|
|
message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')
|
|
}));
|
|
}
|
|
|
|
if (notificationToMarkAsSeen.seen) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
allNotifications[notificationToMarkAsSeenIndex].seen = true;
|
|
|
|
return SettingsAPI.edit({
|
|
settings: [{
|
|
key: 'notifications',
|
|
value: allNotifications
|
|
}]
|
|
}, internalContext);
|
|
})
|
|
.return();
|
|
}
|
|
|
|
tasks = [
|
|
handlePermissions,
|
|
destroyNotification
|
|
];
|
|
|
|
return pipeline(tasks, options);
|
|
},
|
|
|
|
/**
|
|
* ### DestroyAll
|
|
* Clear all notifications, used for tests
|
|
*
|
|
* @private Not exposed over HTTP
|
|
* @returns {Promise}
|
|
*/
|
|
destroyAll: function destroyAll(options) {
|
|
return canThis(options.context).destroy.notification()
|
|
.then(function () {
|
|
return _private.fetchAllNotifications()
|
|
.then(function (allNotifications) {
|
|
_.each(allNotifications, function (notification) {
|
|
notification.seen = true;
|
|
});
|
|
|
|
return SettingsAPI.edit({
|
|
settings: [{
|
|
key: 'notifications',
|
|
value: allNotifications
|
|
}]
|
|
}, internalContext);
|
|
})
|
|
.return();
|
|
}, function (err) {
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
err: err,
|
|
context: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')
|
|
}));
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = notifications;
|