Fix - subscriber custom data were not listed in correct order in the subcribers list
"Test user" field added to segment rules Configuration option to automatically share arbitrary namespace based on user role.
This commit is contained in:
parent
a1e52c2d7a
commit
0d7f962c86
8 changed files with 91 additions and 57 deletions
|
@ -406,6 +406,11 @@ export function getRuleHelpers(t, fields) {
|
||||||
column: 'latest_click',
|
column: 'latest_click',
|
||||||
name: t('latestClick'),
|
name: t('latestClick'),
|
||||||
type: 'date'
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'is_test',
|
||||||
|
name: t('Test user'),
|
||||||
|
type: 'option'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,24 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
|
||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.then(() =>
|
||||||
|
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () =>
|
||||||
|
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
|
||||||
|
startHTTPServer(AppType.PUBLIC, 'public', publicPort, async () => {
|
||||||
|
|
||||||
|
await privilegeHelpers.ensureMailtrainDir(uploadedFilesDir);
|
||||||
|
|
||||||
|
privilegeHelpers.dropRootPrivileges();
|
||||||
|
|
||||||
|
tzupdate.start();
|
||||||
|
|
||||||
|
log.info('Service', 'All services started');
|
||||||
|
appBuilder.setReady();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
/*
|
||||||
.then(() =>
|
.then(() =>
|
||||||
executor.spawn(() =>
|
executor.spawn(() =>
|
||||||
testServer(() =>
|
testServer(() =>
|
||||||
|
@ -130,6 +148,7 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ const htmlToText = require('html-to-text');
|
||||||
const {getPublicUrl} = require('./urls');
|
const {getPublicUrl} = require('./urls');
|
||||||
const blacklist = require('../models/blacklist');
|
const blacklist = require('../models/blacklist');
|
||||||
const libmime = require('libmime');
|
const libmime = require('libmime');
|
||||||
|
const shares = require('../models/shares')
|
||||||
|
|
||||||
|
|
||||||
class CampaignSender {
|
class CampaignSender {
|
||||||
|
@ -30,7 +31,7 @@ class CampaignSender {
|
||||||
let sendConfiguration, list, fieldsGrouped, campaign, subscriptionGrouped, useVerp, useVerpSenderHeader, mergeTags, attachments;
|
let sendConfiguration, list, fieldsGrouped, campaign, subscriptionGrouped, useVerp, useVerpSenderHeader, mergeTags, attachments;
|
||||||
|
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
sendConfiguration = await sendConfigurations.getByIdTx(tx, context, sendConfigurationId, false, true);
|
sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), sendConfigurationId, false, true);
|
||||||
list = await lists.getByCidTx(tx, context, listCid);
|
list = await lists.getByCidTx(tx, context, listCid);
|
||||||
fieldsGrouped = await fields.listGroupedTx(tx, list.id);
|
fieldsGrouped = await fields.listGroupedTx(tx, list.id);
|
||||||
|
|
||||||
|
@ -42,7 +43,10 @@ class CampaignSender {
|
||||||
|
|
||||||
if (campaignId) {
|
if (campaignId) {
|
||||||
campaign = await campaigns.getByIdTx(tx, context, campaignId, false, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
campaign = await campaigns.getByIdTx(tx, context, campaignId, false, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
||||||
|
await campaigns.enforceSendPermissionTx(tx, context, campaign);
|
||||||
} else {
|
} else {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', sendConfigurationId, 'sendWithoutOverrides');
|
||||||
|
|
||||||
// This is to fake the campaign for getMessageLinks, which is called inside formatMessage
|
// This is to fake the campaign for getMessageLinks, which is called inside formatMessage
|
||||||
campaign = {
|
campaign = {
|
||||||
cid: '[CAMPAIGN_ID]'
|
cid: '[CAMPAIGN_ID]'
|
||||||
|
|
|
@ -19,6 +19,7 @@ const segments = require('./segments');
|
||||||
const senders = require('../lib/senders');
|
const senders = require('../lib/senders');
|
||||||
const {LinkId} = require('./links');
|
const {LinkId} = require('./links');
|
||||||
const feedcheck = require('../lib/feedcheck');
|
const feedcheck = require('../lib/feedcheck');
|
||||||
|
const contextHelpers = require('../lib/context-helpers');
|
||||||
|
|
||||||
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
||||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||||
|
@ -637,8 +638,15 @@ async function remove(context, id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enforceSendPermissionTx(tx, context, campaignId) {
|
async function enforceSendPermissionTx(tx, context, campaignId) {
|
||||||
const campaign = await getByIdTx(tx, context, campaignId, false);
|
let campaign;
|
||||||
const sendConfiguration = await sendConfigurations.getByIdTx(tx, context, campaign.send_configuration, false, false);
|
|
||||||
|
if (typeof campaignId === 'object') {
|
||||||
|
campaign = campaignId;
|
||||||
|
} else {
|
||||||
|
campaign = await getByIdTx(tx, context, campaignId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration, false, false);
|
||||||
|
|
||||||
const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
|
const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
|
||||||
|
|
||||||
|
@ -840,13 +848,13 @@ async function getSubscribersQueryGeneratorTx(tx, campaignId) {
|
||||||
|
|
||||||
async function _changeStatus(context, campaignId, permittedCurrentStates, newState, invalidStateMessage, scheduled = null) {
|
async function _changeStatus(context, campaignId, permittedCurrentStates, newState, invalidStateMessage, scheduled = null) {
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'send');
|
|
||||||
|
|
||||||
const entity = await tx('campaigns').where('id', campaignId).first();
|
const entity = await tx('campaigns').where('id', campaignId).first();
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await enforceSendPermissionTx(tx, context, campaign);
|
||||||
|
|
||||||
if (!permittedCurrentStates.includes(entity.status)) {
|
if (!permittedCurrentStates.includes(entity.status)) {
|
||||||
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
|
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,14 +34,17 @@ async function listDTAjax(context, params) {
|
||||||
.from('lists')
|
.from('lists')
|
||||||
.innerJoin('namespaces', 'namespaces.id', 'lists.namespace'),
|
.innerJoin('namespaces', 'namespaces.id', 'lists.namespace'),
|
||||||
['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name',
|
['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name',
|
||||||
{ query: builder =>
|
{
|
||||||
builder.from('campaigns')
|
name: 'triggerCount',
|
||||||
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
query: builder =>
|
||||||
.innerJoin('triggers', 'campaigns.id', 'triggers.campaign')
|
builder.from('campaigns')
|
||||||
.innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`)
|
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
||||||
.whereRaw('campaign_lists.list = lists.id')
|
.innerJoin('triggers', 'campaigns.id', 'triggers.campaign')
|
||||||
.where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers')
|
.innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`)
|
||||||
.count()
|
.whereRaw('campaign_lists.list = lists.id')
|
||||||
|
.where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers')
|
||||||
|
.count()
|
||||||
|
.as('triggerCount')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,6 +35,10 @@ const predefColumns = [
|
||||||
{
|
{
|
||||||
column: 'latest_click',
|
column: 'latest_click',
|
||||||
type: 'date'
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'is_test',
|
||||||
|
type: 'option'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const knex = require('../lib/knex');
|
const knex = require('../lib/knex');
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const { enforce } = require('../lib/helpers');
|
const { enforce, castToInteger } = require('../lib/helpers');
|
||||||
const dtHelpers = require('../lib/dt-helpers');
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
const entitySettings = require('../lib/entity-settings');
|
const entitySettings = require('../lib/entity-settings');
|
||||||
const interoperableErrors = require('../../shared/interoperable-errors');
|
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||||
|
@ -170,56 +170,45 @@ async function rebuildPermissionsTx(tx, restriction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Reset root and own namespace shares as per the user roles
|
// Reset root, own and shared namespaces shares as per the user roles
|
||||||
const usersWithRoleInOwnNamespaceQuery = tx('users')
|
const usersAutoSharesQry = tx('users')
|
||||||
.leftJoin(namespaceEntityType.sharesTable, {
|
.select(['users.id', 'users.role', 'users.namespace']);
|
||||||
'users.id': `${namespaceEntityType.sharesTable}.user`,
|
|
||||||
'users.namespace': `${namespaceEntityType.sharesTable}.entity`
|
|
||||||
})
|
|
||||||
.select(['users.id', 'users.namespace', 'users.role as userRole', `${namespaceEntityType.sharesTable}.role`]);
|
|
||||||
if (restriction.userId) {
|
if (restriction.userId) {
|
||||||
usersWithRoleInOwnNamespaceQuery.where('users.id', restriction.userId);
|
usersAutoSharesQry.where('users.id', restriction.userId);
|
||||||
}
|
}
|
||||||
const usersWithRoleInOwnNamespace = await usersWithRoleInOwnNamespaceQuery;
|
const usersAutoShares = await usersAutoSharesQry;
|
||||||
|
|
||||||
for (const user of usersWithRoleInOwnNamespace) {
|
for (const user of usersAutoShares) {
|
||||||
const roleConf = config.roles.global[user.userRole];
|
const roleConf = config.roles.global[user.role];
|
||||||
|
|
||||||
if (roleConf) {
|
if (roleConf) {
|
||||||
const desiredRole = roleConf.ownNamespaceRole;
|
const desiredRoles = new Map();
|
||||||
if (desiredRole && user.role !== desiredRole) {
|
|
||||||
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: user.namespace }).del();
|
if (roleConf.sharedNamespaces) {
|
||||||
await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: user.namespace, role: desiredRole, auto: true });
|
for (const shrKey in roleConf.sharedNamespaces) {
|
||||||
|
const shrRole = roleConf.sharedNamespaces[shrKey];
|
||||||
|
const shrNsId = castToInteger(shrKey);
|
||||||
|
|
||||||
|
desiredRoles.set(shrNsId, shrRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleConf.ownNamespaceRole) {
|
||||||
|
desiredRoles.set(user.namespace, roleConf.ownNamespaceRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleConf.rootNamespaceRole) {
|
||||||
|
desiredRoles.set(getGlobalNamespaceId(), roleConf.rootNamespaceRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [nsId, role] of desiredRoles.entries()) {
|
||||||
|
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: nsId }).del();
|
||||||
|
await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: nsId, role: role, auto: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const usersWithRoleInRootNamespaceQuery = tx('users')
|
|
||||||
.leftJoin(namespaceEntityType.sharesTable, {
|
|
||||||
'users.id': `${namespaceEntityType.sharesTable}.user`,
|
|
||||||
[`${namespaceEntityType.sharesTable}.entity`]: getGlobalNamespaceId()
|
|
||||||
})
|
|
||||||
.select(['users.id', 'users.role as userRole', `${namespaceEntityType.sharesTable}.role`]);
|
|
||||||
if (restriction.userId) {
|
|
||||||
usersWithRoleInRootNamespaceQuery.andWhere('users.id', restriction.userId);
|
|
||||||
}
|
|
||||||
const usersWithRoleInRootNamespace = await usersWithRoleInRootNamespaceQuery;
|
|
||||||
|
|
||||||
for (const user of usersWithRoleInRootNamespace) {
|
|
||||||
const roleConf = config.roles.global[user.userRole];
|
|
||||||
|
|
||||||
if (roleConf) {
|
|
||||||
const desiredRole = roleConf.rootNamespaceRole;
|
|
||||||
if (desiredRole && user.role !== desiredRole) {
|
|
||||||
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: getGlobalNamespaceId() }).del();
|
|
||||||
await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: getGlobalNamespaceId(), role: desiredRole, auto: 1 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Build the map of all namespaces
|
// Build the map of all namespaces
|
||||||
// nsMap is a map of namespaces - each of the following shape:
|
// nsMap is a map of namespaces - each of the following shape:
|
||||||
// .id - id of the namespace
|
// .id - id of the namespace
|
||||||
|
|
|
@ -262,7 +262,7 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
listTable + '.email',
|
listTable + '.email',
|
||||||
listTable + '.status',
|
listTable + '.status',
|
||||||
listTable + '.created',
|
listTable + '.created',
|
||||||
{ name: 'blacklisted', raw: 'not isnull(blacklist.email)' }
|
{ name: 'blacklisted', raw: 'not isnull(blacklist.email) as `blacklisted`' }
|
||||||
];
|
];
|
||||||
const extraColumns = [];
|
const extraColumns = [];
|
||||||
let listFldIdx = columns.length;
|
let listFldIdx = columns.length;
|
||||||
|
@ -275,9 +275,11 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
if (fld.column) {
|
if (fld.column) {
|
||||||
columns.push(listTable + '.' + fld.column);
|
columns.push(listTable + '.' + fld.column);
|
||||||
} else {
|
} else {
|
||||||
|
const colName = listTable + '.' + fldCol;
|
||||||
|
|
||||||
columns.push({
|
columns.push({
|
||||||
name: listTable + '.' + fldCol,
|
name: colName,
|
||||||
raw: '?',
|
raw: '? as `' + colName + '`',
|
||||||
data: [0]
|
data: [0]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue