/**
* This module is meant for handling date presets.
* @module api/parts/mgmt/date_presets
*/
/** @lends module:api/parts/mgmt/date_presets */
var presetsApi = {},
common = require('../../utils/common.js');
/**
* Get presets
* @param {params} params - params object
* @returns {boolean} true if successful
**/
presetsApi.getAll = async function(params) {
var member = params.member,
memberId = member._id + "",
memberEmail = member.email,
filterCond = {};
if (!member.global_admin) {
var groups = member.group_id || [];
if (!Array.isArray(groups)) {
groups = [groups];
}
groups = groups.map(group_id => group_id + "");
filterCond = {
$or: [
{owner_id: memberId},
{share_with: "all-users"},
{shared_email_view: memberEmail},
{shared_email_edit: memberEmail},
{shared_user_groups_edit: {$in: groups}},
{shared_user_groups_view: {$in: groups}}
]
};
}
common.db.collection('date_presets').aggregate([
{ $match: filterCond },
{
$addFields: {
owner_id_objectId: { $toObjectId: '$owner_id' }
}
},
{
$lookup: {
from: 'members',
localField: 'owner_id_objectId',
foreignField: '_id',
as: 'owner'
}
},
{ $unwind: '$owner' },
{
$project: {
_id: 1,
name: 1,
range: 1,
share_with: 1,
shared_email_edit: {
$cond: {
if: {
$or: [
member.global_admin,
{ $eq: ['$owner_id', memberId] },
{ $in: [groups, '$shared_user_groups_edit'] },
{ $in: [memberEmail, '$shared_email_edit'] }
]
},
then: '$shared_email_edit',
else: false
}
},
shared_email_view: {
$cond: {
if: {
$or: [
member.global_admin,
{ $eq: ['$owner_id', memberId] },
{ $in: [groups, '$shared_user_groups_edit'] },
{ $in: [memberEmail, '$shared_email_edit'] }
]
},
then: '$shared_email_view',
else: false
}
},
shared_user_groups_edit: {
$cond: {
if: {
$or: [
member.global_admin,
{ $eq: ['$owner_id', memberId] },
{ $in: [groups, '$shared_user_groups_edit'] },
{ $in: [memberEmail, '$shared_email_edit']}
]
},
then: '$shared_user_groups_edit',
else: false
}
},
shared_user_groups_view: {
$cond: {
if: {
$or: [
member.global_admin,
{ $eq: ['$owner_id', memberId] },
{ $in: [groups, '$shared_user_groups_edit'] },
{ $in: [memberEmail, '$shared_email_edit']}
]
},
then: '$shared_user_groups_view',
else: false
}
},
exclude_current_day: 1,
owner_id: 1,
owner_name: '$owner.full_name',
fav: {
$in: [memberId, '$fav']
},
is_owner: {
$cond: {
if: {
$eq: ['$owner_id', memberId]
},
then: true,
else: member.global_admin
}
},
created_at: 1,
edited_at: 1,
sort_order: 1
}
},
{
$sort: {
sort_order: 1
}
}
]).toArray(function(err, presets) {
if (err || !presets || !presets.length) {
console.log("Error getting presets", err, presets);
common.returnMessage(params, 500, 'Error getting presets');
return false;
}
common.returnOutput(params, presets);
return true;
});
return true;
};
/**
* Add new preset
* @param {params} params - params object
* @returns {boolean} true if successful
**/
presetsApi.create = function(params) {
var argProps = {
'name': {
'required': true,
'type': 'String',
'non-empty': true
},
'range': {
'required': true,
'type': [
'Array',
'Object',
'String'
],
'multiple': true
},
'share_with': {
'required': true,
'type': 'String',
'default': 'none', //none, all-users, selected-users
'non-empty': true
},
'shared_email_edit': {
'required': false,
'type': 'Array'
},
'shared_email_view': {
'required': false,
'type': 'Array'
},
'shared_user_groups_edit': {
'required': false,
'type': 'Array'
},
'shared_user_groups_view': {
'required': false,
'type': 'Array'
},
'exclude_current_day': {
'required': false,
'type': 'Boolean',
'default': false
},
'app_id': {
'required': true,
'type': 'String',
'min-length': 24,
'max-length': 24,
'exclude-from-ret-obj': true
}
},
newPreset = {};
if (params.qstring.range) {
try {
params.qstring.range = JSON.parse(params.qstring.range);
if (Array.isArray(params.qstring.range) && params.qstring.range.length !== 2) {
throw new SyntaxError();
}
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid range');
return false;
}
}
if (params.qstring.shared_email_edit) {
try {
params.qstring.shared_email_edit = JSON.parse(params.qstring.shared_email_edit);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_email_edit');
return false;
}
}
if (params.qstring.shared_email_view) {
try {
params.qstring.shared_email_view = JSON.parse(params.qstring.shared_email_view);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_email_view');
return false;
}
}
if (params.qstring.shared_user_groups_edit) {
try {
params.qstring.shared_user_groups_edit = JSON.parse(params.qstring.shared_user_groups_edit);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_user_groups_edit');
return false;
}
}
if (params.qstring.shared_user_groups_view) {
try {
params.qstring.shared_user_groups_view = JSON.parse(params.qstring.shared_user_groups_view);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_user_groups_view');
return false;
}
}
if (params.qstring.exclude_current_day) {
try {
params.qstring.exclude_current_day = JSON.parse(params.qstring.exclude_current_day);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid exclude_current_day');
return false;
}
}
if (!(newPreset = common.validateArgs(params.qstring, argProps))) {
common.returnMessage(params, 400, 'Not enough args');
return false;
}
newPreset.created_at = Math.floor(((new Date()).getTime()) / 1000);
newPreset.owner_id = params.member._id + "";
newPreset.fav = [];
if (newPreset.share_with !== "selected-users") {
newPreset.shared_email_edit = [];
newPreset.shared_email_view = [];
newPreset.shared_user_groups_edit = [];
newPreset.shared_user_groups_view = [];
}
newPreset.sort_order = 0;
common.db.collection('date_presets').insert(newPreset, { safe: true }, function(err, preset) {
if (!err && preset && preset.ops && preset.ops[0] && preset.ops[0]._id) {
//Update sort order of other presets
common.db.collection('date_presets').updateMany({_id: {$ne: preset.ops[0]._id}}, {$inc: {sort_order: 1}}, function(err1) {
if (err1) {
common.returnMessage(params, 500, 'Error updating sort order of other presets');
return false;
}
common.returnOutput(params, preset.ops[0]);
return true;
});
}
else {
common.returnMessage(params, 500, 'Error adding preset');
return false;
}
});
};
/**
* Update preset
* @param {params} params - params object
* @returns {boolean} true if successful
**/
presetsApi.update = function(params) {
var argProps = {
'preset_id': {
'required': true,
'type': 'String',
'min-length': 24,
'max-length': 24,
'exclude-from-ret-obj': true
},
'name': {
'required': false,
'type': 'String',
'non-empty': true
},
'range': {
'required': false,
'type': [
'Array',
'Object',
'String'
],
'multiple': true
},
'share_with': {
'required': false,
'type': 'String',
'default': 'none', //none, all-users, selected-users
'non-empty': true
},
'shared_email_edit': {
'required': false,
'type': 'Array'
},
'shared_email_view': {
'required': false,
'type': 'Array'
},
'shared_user_groups_edit': {
'required': false,
'type': 'Array'
},
'shared_user_groups_view': {
'required': false,
'type': 'Array'
},
'exclude_current_day': {
'required': false,
'type': 'Boolean',
'default': false
},
'fav': {
'required': false,
'type': 'Boolean',
},
'sort_order': {
'required': false,
'type': 'Number'
},
'app_id': {
'required': true,
'type': 'String',
'min-length': 24,
'max-length': 24,
'exclude-from-ret-obj': true
}
},
updatedPreset = {},
member = params.member,
memberId = member._id + "",
memberEmail = member.email,
filterCond = {};
var groups = member.group_id || [];
if (!Array.isArray(groups)) {
groups = [groups];
}
groups = groups.map(group_id => group_id + "");
if (!member.global_admin) {
filterCond = {
$or: [
{owner_id: memberId},
{share_with: "all-users"},
{shared_email_view: memberEmail},
{shared_email_edit: memberEmail},
{shared_user_groups_edit: {$in: groups}},
{shared_user_groups_view: {$in: groups}}
]
};
}
if (params.qstring.range) {
try {
params.qstring.range = JSON.parse(params.qstring.range);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid range');
return false;
}
}
if (params.qstring.shared_email_edit) {
try {
params.qstring.shared_email_edit = JSON.parse(params.qstring.shared_email_edit);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_email_edit');
return false;
}
}
if (params.qstring.shared_email_view) {
try {
params.qstring.shared_email_view = JSON.parse(params.qstring.shared_email_view);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_email_view');
return false;
}
}
if (params.qstring.shared_user_groups_edit) {
try {
params.qstring.shared_user_groups_edit = JSON.parse(params.qstring.shared_user_groups_edit);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_user_groups_edit');
return false;
}
}
if (params.qstring.shared_user_groups_view) {
try {
params.qstring.shared_user_groups_view = JSON.parse(params.qstring.shared_user_groups_view);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid shared_user_groups_view');
return false;
}
}
if (params.qstring.exclude_current_day) {
try {
params.qstring.exclude_current_day = JSON.parse(params.qstring.exclude_current_day);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid exclude_current_day');
return false;
}
}
if (params.qstring.fav) {
try {
params.qstring.fav = JSON.parse(params.qstring.fav);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid fav');
return false;
}
}
if (params.qstring.sort_order) {
try {
params.qstring.sort_order = JSON.parse(params.qstring.sort_order);
}
catch (SyntaxError) {
common.returnMessage(params, 400, 'Invalid sort_order');
return false;
}
}
if (!(updatedPreset = common.validateArgs(params.qstring, argProps))) {
common.returnMessage(params, 400, 'Not enough args');
return false;
}
filterCond._id = common.db.ObjectID(params.qstring.preset_id);
common.db.collection('date_presets').findOne(filterCond, function(err, presetBefore) {
if (err || !presetBefore) {
common.returnMessage(params, 500, 'Could not find preset');
return false;
}
//The selector above is wider than edit rights on purpose, so it cannot decide
//them. Two changes are open to anybody who can see a preset, and both reach
//this same handler: marking it a favourite, which is stored on the preset as an
//entry in fav per member, and dragging it in the management table, which writes
//the shared sort_order. Everything else belongs to the owner and the members
//named in the edit lists, which is what the drawer offers and what the delete
//handler already requires.
//
//Both of those send the whole row, so a viewer's request carries the fields
//they may not change as well. Those are dropped rather than refused, because
//refusing would fail the two actions the UI does offer them.
var canEdit = canEditPreset(presetBefore, member, groups);
if (canEdit) {
updatedPreset.edited_at = Math.floor(((new Date()).getTime()) / 1000);
if (updatedPreset.share_with !== "selected-users") {
updatedPreset.shared_email_edit = [];
updatedPreset.shared_email_view = [];
updatedPreset.shared_user_groups_edit = [];
updatedPreset.shared_user_groups_view = [];
}
}
else {
//sort_order is the list's shared ordering, and the management table lets
//anybody who can see a preset drag it, so it stays writable here next to
//fav. Everything the report is about, the content, the sharing lists and
//owner_id, does not.
updatedPreset = typeof updatedPreset.sort_order === "undefined" ? {} : {sort_order: updatedPreset.sort_order};
}
//Handle fav
if (typeof params.qstring.fav !== 'undefined') {
let fav = presetBefore.fav || [];
if (params.qstring.fav === true && !fav.includes(params.member._id + "")) {
fav.push(params.member._id + "");
}
if (params.qstring.fav === false && fav.includes(params.member._id + "")) {
fav.splice(fav.indexOf(params.member._id + ""), 1);
}
updatedPreset.fav = fav;
}
if (Object.keys(updatedPreset).length === 0) {
common.returnMessage(params, 403, 'Not allowed to edit this preset');
return false;
}
common.db.collection('date_presets').update({_id: presetBefore._id}, {$set: updatedPreset}, {safe: true}, function(err1) {
if (err1) {
common.returnMessage(params, 500, 'Error updating preset');
return false;
}
//If sort order changed, update other presets
if (updatedPreset.sort_order !== presetBefore.sort_order) {
const oldSortOrder = presetBefore.sort_order;
const newSortOrder = updatedPreset.sort_order;
const sort_order_difference = oldSortOrder - newSortOrder;
const updateQuery = {
sort_order: {
$gte: Math.min(oldSortOrder, newSortOrder),
$lte: Math.max(oldSortOrder, newSortOrder)
},
_id: { $ne: presetBefore._id }
};
const update = sort_order_difference > 0 ? { $inc: { sort_order: 1 } } : { $inc: { sort_order: -1 } };
common.db.collection('date_presets').updateMany(updateQuery, update, {multi: true}, function(err2) {
if (err2) {
common.returnMessage(params, 500, 'Error updating sort order of other presets');
return false;
}
common.returnMessage(params, 200, 'Preset updated');
return true;
});
}
else {
common.returnMessage(params, 200, 'Preset updated');
return true;
}
});
});
return true;
};
/**
* Get presets
* @param {params} params - params object
* @returns {boolean} true if successful
**/
presetsApi.delete = function(params) {
var argProps = {
'preset_id': {
'required': true,
'type': 'String',
'min-length': 24,
'max-length': 24,
'exclude-from-ret-obj': true
}
};
if (!common.validateArgs(params.qstring, argProps)) {
common.returnMessage(params, 400, 'Not enough args');
return false;
}
common.db.collection('date_presets').findOne({ _id: common.db.ObjectID(params.qstring.preset_id)}, function(err, presetBefore) {
if (err || !presetBefore) {
common.returnMessage(params, 500, 'Could not find preset');
return false;
}
if (presetBefore.owner_id !== params.member._id + "" && !params.member.global_admin) {
common.returnMessage(params, 401, 'Not authorized');
return false;
}
common.db.collection('date_presets').remove({_id: common.db.ObjectID(params.qstring.preset_id)}, { safe: true }, function(err1, preset) {
if (err1 || !preset) {
common.returnMessage(params, 500, 'Error deleting preset');
return false;
}
//Update sort order of other presets
common.db.collection('date_presets').updateMany({sort_order: {$gt: presetBefore.sort_order}}, {$inc: {sort_order: -1}}, function(err2) {
if (err2) {
common.returnMessage(params, 500, 'Error updating sort order of other presets');
return false;
}
common.returnOutput(params, preset);
return true;
});
});
});
};
presetsApi.getById = function(params) {
var argProps = {
'preset_id': {
'required': true,
'type': 'String',
'min-length': 24,
'max-length': 24,
'exclude-from-ret-obj': true
},
'app_id': {
'required': true,
'type': 'String',
'min-length': 24,
'max-length': 24,
'exclude-from-ret-obj': true
}
};
var member = params.member,
memberId = member._id + "",
memberEmail = member.email,
filterCond = {};
if (!member.global_admin) {
var groups = member.group_id || [];
if (!Array.isArray(groups)) {
groups = [groups];
}
groups = groups.map(group_id => group_id + "");
filterCond = {
$or: [
{owner_id: memberId},
{share_with: "all-users"},
{shared_email_view: memberEmail},
{shared_email_edit: memberEmail},
{shared_user_groups_edit: {$in: groups}},
{shared_user_groups_view: {$in: groups}}
]
};
}
if (!common.validateArgs(params.qstring, argProps)) {
common.returnMessage(params, 400, 'Not enough args');
return false;
}
filterCond._id = common.db.ObjectID(params.qstring.preset_id);
common.db.collection('date_presets').findOne(filterCond, {projection: PRESET_FIELDS}, function(err, preset) {
if (err || !preset) {
common.returnMessage(params, 500, 'Could not find preset');
return false;
}
preset.is_owner = (preset.owner_id === memberId) || member.global_admin;
//who a preset is shared with is shown only to those who may edit it, as in getAll
if (!canEditPreset(preset, member, groups || [])) {
preset.shared_email_edit = false;
preset.shared_email_view = false;
preset.shared_user_groups_edit = false;
preset.shared_user_groups_view = false;
}
let fav = preset.fav || [];
preset.fav = fav.includes(memberId);
common.returnOutput(params, preset);
return true;
});
};
/**
* The fields a preset is made of. Used as a projection so a stored document cannot
* return anything beyond them, and as the shape both write handlers persist.
**/
const PRESET_FIELDS = {
name: 1,
range: 1,
exclude_current_day: 1,
share_with: 1,
shared_email_edit: 1,
shared_email_view: 1,
shared_user_groups_edit: 1,
shared_user_groups_view: 1,
owner_id: 1,
fav: 1,
sort_order: 1,
created_at: 1,
edited_at: 1
};
/**
* Whether a member may change a preset rather than only see it. Mirrors what the
* drawer offers and what the delete handler already requires: the owner, a global
* admin, or somebody named in one of the edit lists. Being in a view list, or the
* preset being shared with all users, grants no write.
* @param {object} preset - the stored preset
* @param {object} member - params.member
* @param {array} groups - the member's group ids, as strings
* @returns {boolean} true when the member may edit the preset
**/
function canEditPreset(preset, member, groups) {
if (member.global_admin || preset.owner_id === member._id + "") {
return true;
}
if ((preset.shared_email_edit || []).includes(member.email)) {
return true;
}
return (preset.shared_user_groups_edit || []).some(function(group) {
return groups.includes(group + "");
});
}
module.exports = presetsApi;