- Fix for #890
- "Channels" feature - Shoutout config param rendered on the homepage - "Clone" feature for campaigns
This commit is contained in:
parent
00432e6cfe
commit
d170548cfa
25 changed files with 1009 additions and 525 deletions
|
@ -298,7 +298,7 @@ defaultRoles:
|
|||
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, createCampaign, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
|
@ -308,13 +308,13 @@ defaultRoles:
|
|||
|
||||
campaignsAdmin:
|
||||
name: Campaigns Admin
|
||||
description: In the respective namespace, the user has all permissions for managing lists, templates and campaigns and the permission to send to send configurations.
|
||||
description: In the respective namespace, the user has all permissions for managing lists, channels, templates and campaigns and the permission to send to send configurations.
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createChannel, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic, sendWithoutOverrides, sendWithAllowedOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, createCampaign, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
|
@ -325,7 +325,7 @@ defaultRoles:
|
|||
campaignsCreator:
|
||||
name: Campaigns Creator
|
||||
description: In the respective namespace, the user has all permissions to create and manage templates and campaigns. The user can also read public data about send configurations and use Mosaico templates in the namespace.
|
||||
permissions: [view, createTemplate, createChannel, createCampaign]
|
||||
permissions: [view, createTemplate, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic]
|
||||
channel: [view]
|
||||
|
|
|
@ -11,14 +11,14 @@ async function validateEntity(tx, entity) {
|
|||
}
|
||||
}
|
||||
|
||||
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
|
||||
async function validateMoveTx(tx, context, entity, existing, entityTypeId, createOperation, deleteOperation) {
|
||||
if (existing.namespace !== entity.namespace) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
|
||||
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, createOperation);
|
||||
await shares.enforceEntityPermissionTx(tx, context, entityTypeId, entity.id, deleteOperation);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateEntity,
|
||||
validateMove
|
||||
validateMoveTx
|
||||
};
|
|
@ -357,7 +357,7 @@ async function rawGetByTx(tx, key, id) {
|
|||
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
||||
.groupBy('campaigns.id')
|
||||
.select([
|
||||
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
|
||||
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.channel', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
|
||||
'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject',
|
||||
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url', 'campaigns.scheduled',
|
||||
'campaigns.delivered', 'campaigns.unsubscribed', 'campaigns.bounced', 'campaigns.complained', 'campaigns.blacklisted', 'campaigns.opened', 'campaigns.clicks',
|
||||
|
@ -412,6 +412,7 @@ async function getByIdTx(tx, context, id, withPermissions = true, content = Cont
|
|||
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
|
||||
entity = {
|
||||
id: entity.id,
|
||||
channel: entity.channel,
|
||||
send_configuration: entity.send_configuration,
|
||||
|
||||
data: {
|
||||
|
@ -454,6 +455,8 @@ async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
|
|||
|
||||
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
|
||||
}
|
||||
|
||||
enforce(Number.isInteger(entity.source));
|
||||
|
@ -481,6 +484,10 @@ async function _createTx(tx, context, entity, content) {
|
|||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
|
||||
|
||||
if (entity.channel) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.channel, 'createCampaign');
|
||||
}
|
||||
|
||||
let copyFilesFrom = null;
|
||||
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
copyFilesFrom = {
|
||||
|
@ -570,6 +577,13 @@ async function createRssTx(tx, context, entity) {
|
|||
return await _createTx(tx, context, entity, Content.RSS_ENTRY);
|
||||
}
|
||||
|
||||
async function _validateChannelMoveTx(tx, context, entity, existing) {
|
||||
if (existing.channel !== entity.channel) {
|
||||
await shares.enforceEntityPermission(context, 'channel', entity.channel, 'createCampaign');
|
||||
await shares.enforceEntityPermission(context, 'campaign', entity.id, 'delete');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity, content) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit');
|
||||
|
@ -585,11 +599,13 @@ async function updateWithConsistencyCheck(context, entity, content) {
|
|||
|
||||
let filteredEntity = filterObject(entity, allowedKeysUpdate);
|
||||
if (content === Content.ALL) {
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'campaign', 'createCampaign', 'delete');
|
||||
await _validateChannelMoveTx(tx, context, entity, existing);
|
||||
|
||||
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
filteredEntity.data.sourceCustom = existing.data.sourceCustom;
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'campaign', 'createCampaign', 'delete');
|
||||
await _validateChannelMoveTx(tx, context, entity, existing);
|
||||
|
||||
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
|
||||
const data = existing.data;
|
||||
|
|
|
@ -16,8 +16,8 @@ const dependencyHelpers = require('../lib/dependency-helpers');
|
|||
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
|
||||
const activityLog = require('../lib/activity-log');
|
||||
|
||||
const allowedKeys = ['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source'];
|
||||
const allowedKeys = new Set(['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source']);
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
|
@ -43,12 +43,27 @@ async function listDTAjax(context, params) {
|
|||
);
|
||||
}
|
||||
|
||||
async function _getByTx(tx, key, id, withPermissions = true) {
|
||||
async function listWithCreateCampaignPermissionDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'channel', requiredOperations: ['createCampaign'] }],
|
||||
params,
|
||||
builder => {
|
||||
builder = builder.from('channels')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'channels.namespace');
|
||||
return builder;
|
||||
},
|
||||
['channels.id', 'channels.name', 'channels.cid', 'channels.description', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async function _getByTx(tx, context, key, id, withPermissions = true) {
|
||||
const entity = await tx('channels').where('channels.' + key, id)
|
||||
.leftJoin('channel_lists', 'channels.id', 'channel_lists.channel')
|
||||
.groupBy('channels.id')
|
||||
.select([
|
||||
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.source',
|
||||
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.cpg_name', 'channels.cpg_description', 'channels.source',
|
||||
'channels.send_configuration', 'channels.from_name_override', 'channels.from_email_override', 'channels.reply_to_override', 'channels.subject',
|
||||
'channels.data', 'channels.click_tracking_disabled', 'channels.open_tracking_disabled', 'channels.unsubscribe_url',
|
||||
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', channel_lists.list, channel_lists.segment) ORDER BY channel_lists.id SEPARATOR \';\') as lists`)
|
||||
|
@ -82,7 +97,7 @@ async function _getByTx(tx, key, id, withPermissions = true) {
|
|||
async function getByIdTx(tx, context, id, withPermissions = true) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'view');
|
||||
|
||||
return await _getByTx(tx, 'id', id, withPermissions);
|
||||
return await _getByTx(tx, context, 'id', id, withPermissions);
|
||||
}
|
||||
|
||||
async function getById(context, id, withPermissions = true) {
|
||||
|
@ -97,12 +112,13 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
|
|||
if (entity.source !== null) {
|
||||
enforce(Number.isInteger(entity.source));
|
||||
|
||||
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
|
||||
} else if (entity.source === CampaignSource.CUSTOM) {
|
||||
enforce(allTagLanguages.includes(entity.data.sourceCustom.tag_language), `Invalid tag language '${entity.data.sourceCustom.tag_language}'`);
|
||||
} else if (entity.source === CampaignSource.URL) {
|
||||
} else {
|
||||
enforce(false, 'Unknown channel source');
|
||||
}
|
||||
|
@ -156,7 +172,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.id, 'edit');
|
||||
|
||||
const existing = await _getByTx(tx, 'id', entity.id, false);
|
||||
const existing = await _getByTx(tx, context, 'id', entity.id, false);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash !== entity.originalHash) {
|
||||
|
@ -166,7 +182,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
await _validateAndPreprocess(tx, context, entity, false);
|
||||
|
||||
let filteredEntity = filterObject(entity, allowedKeys);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'channel', 'createCampaign', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'channel', 'createCampaign', 'delete');
|
||||
|
||||
await tx('channel_lists').where('channel', entity.id).del();
|
||||
await tx('channel_lists').insert(entity.lists.map(x => ({channel: entity.id, ...x})));
|
||||
|
@ -197,8 +213,8 @@ async function remove(context, id) {
|
|||
|
||||
|
||||
module.exports.hash = hash;
|
||||
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listWithCreateCampaignPermissionDTAjax = listWithCreateCampaignPermissionDTAjax;
|
||||
module.exports.getByIdTx = getByIdTx;
|
||||
module.exports.getById = getById;
|
||||
module.exports.create = create;
|
||||
|
|
|
@ -169,7 +169,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'customForm', 'createCustomForm', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'customForm', 'createCustomForm', 'delete');
|
||||
|
||||
const form = filterObject(entity, allowedFormKeys);
|
||||
enforce(!Object.keys(checkForMjmlErrors(form)).length, 'Error(s) in form templates');
|
||||
|
|
|
@ -263,7 +263,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
|
||||
await _validateAndPreprocess(tx, entity);
|
||||
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'list', 'createList', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'list', 'createList', 'delete');
|
||||
|
||||
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
|
||||
await _validateAndPreprocess(tx, entity);
|
||||
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
|
||||
|
||||
await tx('mosaico_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
// namespaceHelpers.validateEntity is not needed here because it is part of the tree traversal check below
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'namespace', 'createNamespace', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'namespace', 'createNamespace', 'delete');
|
||||
|
||||
let iter = entity;
|
||||
while (iter.namespace != null) {
|
||||
|
|
|
@ -66,7 +66,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'reportTemplate', 'createReportTemplate', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'reportTemplate', 'createReportTemplate', 'delete');
|
||||
|
||||
await tx('report_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'report', 'createReport', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'report', 'createReport', 'delete');
|
||||
|
||||
entity.params = JSON.stringify(entity.params);
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
|
||||
await _validateAndPreprocess(tx, entity);
|
||||
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'sendConfiguration', 'createSendConfiguration', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'sendConfiguration', 'createSendConfiguration', 'delete');
|
||||
|
||||
await tx('send_configurations').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
await _validateAndPreprocess(tx, entity);
|
||||
entity.data = JSON.stringify(entity.data);
|
||||
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');
|
||||
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'template', 'createTemplate', 'delete');
|
||||
|
||||
const filteredEntity = filterObject(entity, allowedKeys);
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ router.postAsync('/channels-table', passport.loggedIn, async (req, res) => {
|
|||
return res.json(await channels.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/channels-with-create-campaign-permission-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await channels.listWithCreateCampaignPermissionDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.getAsync('/channels/:channelId', passport.loggedIn, async (req, res) => {
|
||||
const channel = await channels.getById(req.context, castToInteger(req.params.channelId), true);
|
||||
channel.hash = channels.hash(channel);
|
||||
|
@ -25,7 +29,7 @@ router.putAsync('/channels/:channelId', passport.loggedIn, passport.csrfProtecti
|
|||
const entity = req.body;
|
||||
entity.id = castToInteger(req.params.channelId);
|
||||
|
||||
await channels.updateWithConsistencyCheck(req.context);
|
||||
await channels.updateWithConsistencyCheck(req.context, entity);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue