mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
parent
b899a6fec8
commit
9f2d68a027
7 changed files with 492 additions and 8 deletions
|
@ -31,6 +31,10 @@ module.exports = {
|
|||
return shared.pipeline(require('./posts'), localUtils);
|
||||
},
|
||||
|
||||
get notifications() {
|
||||
return shared.pipeline(require('./notifications'), localUtils);
|
||||
},
|
||||
|
||||
get settings() {
|
||||
return shared.pipeline(require('./settings'), localUtils);
|
||||
}
|
||||
|
|
196
core/server/api/v2/notifications.js
Normal file
196
core/server/api/v2/notifications.js
Normal file
|
@ -0,0 +1,196 @@
|
|||
const moment = require('moment-timezone');
|
||||
const semver = require('semver');
|
||||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const settingsCache = require('../../services/settings/cache');
|
||||
const ghostVersion = require('../../lib/ghost-version');
|
||||
const common = require('../../lib/common');
|
||||
const ObjectId = require('bson-objectid');
|
||||
const api = require('./index');
|
||||
const internalContext = {context: {internal: true}};
|
||||
const _private = {};
|
||||
|
||||
_private.fetchAllNotifications = () => {
|
||||
let allNotifications = settingsCache.get('notifications');
|
||||
|
||||
allNotifications.forEach((notification) => {
|
||||
notification.addedAt = moment(notification.addedAt).toDate();
|
||||
});
|
||||
|
||||
return allNotifications;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
docName: 'notifications',
|
||||
|
||||
browse: {
|
||||
permissions: true,
|
||||
query() {
|
||||
let allNotifications = _private.fetchAllNotifications();
|
||||
allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
|
||||
|
||||
allNotifications = allNotifications.filter((notification) => {
|
||||
// CASE: do not return old release notification
|
||||
if (!notification.custom && notification.message) {
|
||||
const notificationVersion = notification.message.match(/(\d+\.)(\d+\.)(\d+)/),
|
||||
blogVersion = ghostVersion.full.match(/^(\d+\.)(\d+\.)(\d+)/);
|
||||
|
||||
if (notificationVersion && blogVersion && semver.gt(notificationVersion[0], blogVersion[0])) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return notification.seen !== true;
|
||||
});
|
||||
|
||||
return allNotifications;
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode(result) {
|
||||
if (result.notifications.length) {
|
||||
return 201;
|
||||
} else {
|
||||
return 200;
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
const defaults = {
|
||||
dismissible: true,
|
||||
location: 'bottom',
|
||||
status: 'alert',
|
||||
id: ObjectId.generate()
|
||||
};
|
||||
|
||||
const overrides = {
|
||||
seen: false,
|
||||
addedAt: moment().toDate()
|
||||
};
|
||||
|
||||
let notificationsToCheck = frame.data.notifications;
|
||||
let addedNotifications = [];
|
||||
|
||||
const allNotifications = _private.fetchAllNotifications();
|
||||
|
||||
notificationsToCheck.forEach((notification) => {
|
||||
const isDuplicate = allNotifications.find((n) => {
|
||||
return n.id === notification.id;
|
||||
});
|
||||
|
||||
if (!isDuplicate) {
|
||||
addedNotifications.push(Object.assign({}, defaults, notification, overrides));
|
||||
}
|
||||
});
|
||||
|
||||
const hasReleaseNotification = notificationsToCheck.find((notification) => {
|
||||
return !notification.custom;
|
||||
});
|
||||
|
||||
// CASE: remove any existing release notifications if a new release notification comes in
|
||||
if (hasReleaseNotification) {
|
||||
_.remove(allNotifications, (el) => {
|
||||
return !el.custom;
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: nothing to add, skip
|
||||
if (!addedNotifications.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const addedReleaseNotifications = addedNotifications.filter((notification) => {
|
||||
return !notification.custom;
|
||||
});
|
||||
|
||||
// CASE: only latest release notification
|
||||
if (addedReleaseNotifications.length > 1) {
|
||||
addedNotifications = addedNotifications.filter((notification) => {
|
||||
return notification.custom;
|
||||
});
|
||||
addedNotifications.push(_.orderBy(addedReleaseNotifications, 'created_at', 'desc')[0]);
|
||||
}
|
||||
|
||||
return api.settings.edit({
|
||||
settings: [{
|
||||
key: 'notifications',
|
||||
value: allNotifications.concat(addedNotifications)
|
||||
}]
|
||||
}, internalContext).then(() => {
|
||||
return addedNotifications;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
destroy: {
|
||||
statusCode: 204,
|
||||
options: ['notification_id'],
|
||||
validation: {
|
||||
options: {
|
||||
notification_id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
const allNotifications = _private.fetchAllNotifications();
|
||||
|
||||
const notificationToMarkAsSeen = allNotifications.find((notification) => {
|
||||
return notification.id === frame.options.notification_id;
|
||||
}),
|
||||
notificationToMarkAsSeenIndex = allNotifications.findIndex((notification) => {
|
||||
return notification.id === frame.options.notification_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 api.settings.edit({
|
||||
settings: [{
|
||||
key: 'notifications',
|
||||
value: allNotifications
|
||||
}]
|
||||
}, internalContext).return();
|
||||
}
|
||||
},
|
||||
|
||||
destroyAll: {
|
||||
statusCode: 204,
|
||||
permissions: {
|
||||
method: 'destroy'
|
||||
},
|
||||
query() {
|
||||
const allNotifications = _private.fetchAllNotifications();
|
||||
|
||||
allNotifications.forEach((notification) => {
|
||||
notification.seen = true;
|
||||
});
|
||||
|
||||
return api.settings.edit({
|
||||
settings: [{
|
||||
key: 'notifications',
|
||||
value: allNotifications
|
||||
}]
|
||||
}, internalContext).return();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -21,5 +21,9 @@ module.exports = {
|
|||
|
||||
get settings() {
|
||||
return require('./settings');
|
||||
},
|
||||
|
||||
get notifications() {
|
||||
return require('./notifications');
|
||||
}
|
||||
};
|
||||
|
|
28
core/server/api/v2/utils/serializers/output/notifications.js
Normal file
28
core/server/api/v2/utils/serializers/output/notifications.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:notifications');
|
||||
|
||||
module.exports = {
|
||||
all(response, apiConfig, frame) {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || !response.length) {
|
||||
frame.response = {
|
||||
notifications: []
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
response.forEach((notification) => {
|
||||
delete notification.seen;
|
||||
delete notification.addedAt;
|
||||
});
|
||||
|
||||
frame.response = {
|
||||
notifications: response
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
|
@ -124,9 +124,9 @@ module.exports = function apiRoutes() {
|
|||
);
|
||||
|
||||
// ## Notifications
|
||||
router.get('/notifications', mw.authAdminAPI, api.http(api.notifications.browse));
|
||||
router.post('/notifications', mw.authAdminAPI, api.http(api.notifications.add));
|
||||
router.del('/notifications/:id', mw.authAdminAPI, api.http(api.notifications.destroy));
|
||||
router.get('/notifications', mw.authAdminAPI, apiv2.http(apiv2.notifications.browse));
|
||||
router.post('/notifications', mw.authAdminAPI, apiv2.http(apiv2.notifications.add));
|
||||
router.del('/notifications/:notification_id', mw.authAdminAPI, apiv2.http(apiv2.notifications.destroy));
|
||||
|
||||
// ## DB
|
||||
router.get('/db', mw.authAdminAPI, api.http(api.db.exportContent));
|
||||
|
|
253
core/test/functional/api/v2/admin/notifications_spec.js
Normal file
253
core/test/functional/api/v2/admin/notifications_spec.js
Normal file
|
@ -0,0 +1,253 @@
|
|||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const testUtils = require('../../../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const config = require('../../../../../../core/server/config');
|
||||
const ghost = testUtils.startGhost;
|
||||
let request;
|
||||
|
||||
describe('Notifications API V2', function () {
|
||||
let ghostServer;
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add', function () {
|
||||
it('creates a new notification and sets default fields', function (done) {
|
||||
const newNotification = {
|
||||
type: 'info',
|
||||
message: 'test notification',
|
||||
custom: true,
|
||||
id: 'customId'
|
||||
};
|
||||
|
||||
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({notifications: [newNotification]})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse.notifications);
|
||||
|
||||
testUtils.API.checkResponse(jsonResponse.notifications[0], 'notification');
|
||||
|
||||
jsonResponse.notifications[0].type.should.equal(newNotification.type);
|
||||
jsonResponse.notifications[0].message.should.equal(newNotification.message);
|
||||
jsonResponse.notifications[0].status.should.equal('alert');
|
||||
jsonResponse.notifications[0].dismissible.should.be.true();
|
||||
should.exist(jsonResponse.notifications[0].location);
|
||||
jsonResponse.notifications[0].location.should.equal('bottom');
|
||||
jsonResponse.notifications[0].id.should.be.a.String();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates duplicate', function (done) {
|
||||
const newNotification = {
|
||||
type: 'info',
|
||||
message: 'add twice',
|
||||
custom: true,
|
||||
id: 'customId-2'
|
||||
};
|
||||
|
||||
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({notifications: [newNotification]})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.notifications);
|
||||
jsonResponse.notifications.should.be.an.Array().with.lengthOf(1);
|
||||
jsonResponse.notifications[0].message.should.equal(newNotification.message);
|
||||
|
||||
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({notifications: [newNotification]})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.notifications);
|
||||
jsonResponse.notifications.should.be.an.Array().with.lengthOf(0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have correct order', function () {
|
||||
const firstNotification = {
|
||||
status: 'alert',
|
||||
type: 'info',
|
||||
custom: true,
|
||||
id: 'firstId',
|
||||
dismissible: true,
|
||||
message: '1'
|
||||
};
|
||||
|
||||
const secondNotification = {
|
||||
status: 'alert',
|
||||
type: 'info',
|
||||
custom: true,
|
||||
id: 'secondId',
|
||||
dismissible: true,
|
||||
message: '2'
|
||||
};
|
||||
|
||||
return request.post(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({notifications: [firstNotification]})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then(() => {
|
||||
return request.post(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({notifications: [secondNotification]})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201);
|
||||
})
|
||||
.then(() => {
|
||||
return request.get(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then(res => {
|
||||
const jsonResponse = res.body;
|
||||
|
||||
jsonResponse.notifications.should.be.an.Array().with.lengthOf(4);
|
||||
jsonResponse.notifications[0].id.should.equal(secondNotification.id);
|
||||
jsonResponse.notifications[1].id.should.equal(firstNotification.id);
|
||||
jsonResponse.notifications[2].id.should.equal('customId-2');
|
||||
jsonResponse.notifications[3].id.should.equal('customId');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete', function () {
|
||||
var newNotification = {
|
||||
type: 'info',
|
||||
message: 'test notification',
|
||||
status: 'alert',
|
||||
custom: true
|
||||
};
|
||||
|
||||
it('deletes a notification', function (done) {
|
||||
// create the notification that is to be deleted
|
||||
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({notifications: [newNotification]})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse.notifications);
|
||||
testUtils.API.checkResponse(jsonResponse.notifications[0], 'notification');
|
||||
jsonResponse.notifications.length.should.eql(1);
|
||||
|
||||
jsonResponse.notifications[0].type.should.equal(newNotification.type);
|
||||
jsonResponse.notifications[0].message.should.equal(newNotification.message);
|
||||
jsonResponse.notifications[0].status.should.equal(newNotification.status);
|
||||
|
||||
// begin delete test
|
||||
request.del(localUtils.API.getApiQuery(`notifications/${jsonResponse.notifications[0].id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect(204)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.body.should.be.empty();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 404 when removing notification with unknown id', function () {
|
||||
return request.del(localUtils.API.getApiQuery('notifications/unknown'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404)
|
||||
.then(res => {
|
||||
res.body.errors[0].message.should.equal('Notification does not exist.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('As Editor', function () {
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.createUser({
|
||||
user: testUtils.DataGenerator.forKnex.createUser({
|
||||
email: 'test+1@ghost.org'
|
||||
}),
|
||||
role: testUtils.DataGenerator.Content.roles[2].name
|
||||
});
|
||||
})
|
||||
.then((user) => {
|
||||
request.user = user;
|
||||
return localUtils.doAuth(request);
|
||||
});
|
||||
});
|
||||
|
||||
it('Add notification', function () {
|
||||
const newNotification = {
|
||||
type: 'info',
|
||||
message: 'test notification',
|
||||
custom: true,
|
||||
id: 'customId'
|
||||
};
|
||||
|
||||
return request.post(localUtils.API.getApiQuery('notifications/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({notifications: [newNotification]})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -934,11 +934,10 @@ startGhost = function startGhost(options) {
|
|||
})
|
||||
.then((clients) => {
|
||||
module.exports.existingData.clients = clients.toJSON();
|
||||
|
||||
return models.User.findAll({columns: ['id']});
|
||||
return models.User.findAll({columns: ['id', 'email']});
|
||||
})
|
||||
.then((users) => {
|
||||
module.exports.existingData.users = users.toJSON();
|
||||
module.exports.existingData.users = users.toJSON(module.exports.context.internal);
|
||||
|
||||
return models.Tag.findAll({columns: ['id']});
|
||||
})
|
||||
|
@ -1009,10 +1008,10 @@ startGhost = function startGhost(options) {
|
|||
.then((clients) => {
|
||||
module.exports.existingData.clients = clients.toJSON();
|
||||
|
||||
return models.User.findAll({columns: ['id']});
|
||||
return models.User.findAll({columns: ['id', 'email']});
|
||||
})
|
||||
.then((users) => {
|
||||
module.exports.existingData.users = users.toJSON();
|
||||
module.exports.existingData.users = users.toJSON(module.exports.context.internal);
|
||||
|
||||
return models.Tag.findAll({columns: ['id']});
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue