Editing of triggers seems to work.
Some further fixes.
This commit is contained in:
parent
ffc26a4836
commit
965f30cea7
23 changed files with 855 additions and 377 deletions
13
UPGRADE.md
Normal file
13
UPGRADE.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
## Migration from Mailtrain v1 to Mailtrain v2
|
||||||
|
|
||||||
|
The migration should almost happen automatically. There are however the following caveats:
|
||||||
|
|
||||||
|
1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`)
|
||||||
|
and update your configs accordingly.
|
||||||
|
|
||||||
|
2. Images uploaded in a template editor (Mosaico, GrapeJS, etc.) need to be manually moved to a new destination (under `client`).
|
||||||
|
For Mosaico, this means to move folders named by a number from `public/mosaico` to `client/public/mosaico`.
|
||||||
|
|
||||||
|
3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/public/mosaico/templates`.
|
||||||
|
|
||||||
|
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
|
|
@ -217,34 +217,28 @@ export default class CUD extends Component {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
|
|
||||||
|
for (const key of state.keys()) {
|
||||||
|
state.setIn([key, 'error'], null);
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.getIn(['name', 'value'])) {
|
if (!state.getIn(['name', 'value'])) {
|
||||||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||||
} else {
|
|
||||||
state.setIn(['name', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.getIn(['list', 'value'])) {
|
if (!state.getIn(['list', 'value'])) {
|
||||||
state.setIn(['list', 'error'], t('List must be selected'));
|
state.setIn(['list', 'error'], t('List must be selected'));
|
||||||
} else {
|
|
||||||
state.setIn(['list', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.getIn(['useSegmentation', 'value']) && !state.getIn(['segment', 'value'])) {
|
if (state.getIn(['useSegmentation', 'value']) && !state.getIn(['segment', 'value'])) {
|
||||||
state.setIn(['segment', 'error'], t('Segment must be selected'));
|
state.setIn(['segment', 'error'], t('Segment must be selected'));
|
||||||
} else {
|
|
||||||
state.setIn(['segment', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.getIn(['send_configuration', 'value'])) {
|
if (!state.getIn(['send_configuration', 'value'])) {
|
||||||
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
|
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
|
||||||
} else {
|
|
||||||
state.setIn(['send_configuration', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) {
|
if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) {
|
||||||
state.setIn(['from_email_override', 'error'], t('"From" email must not be empty'));
|
state.setIn(['from_email_override', 'error'], t('"From" email must not be empty'));
|
||||||
} else {
|
|
||||||
state.setIn(['from_email_override', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -252,12 +246,6 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
const sourceTypeKey = Number.parseInt(state.getIn(['source', 'value']));
|
const sourceTypeKey = Number.parseInt(state.getIn(['source', 'value']));
|
||||||
|
|
||||||
for (const key of state.keys()) {
|
|
||||||
if (key.startsWith('data_')) {
|
|
||||||
state.setIn([key, 'error'], null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
|
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
|
||||||
if (!state.getIn(['data_sourceTemplate', 'value'])) {
|
if (!state.getIn(['data_sourceTemplate', 'value'])) {
|
||||||
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
|
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
|
||||||
|
@ -292,7 +280,6 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
validateNamespace(t, state);
|
validateNamespace(t, state);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitHandler() {
|
async submitHandler() {
|
||||||
|
@ -361,8 +348,11 @@ export default class CUD extends Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (submitResponse) {
|
if (submitResponse) {
|
||||||
|
const sourceTypeKey = Number.parseInt(this.getFormValue('source'));
|
||||||
if (this.props.entity) {
|
if (this.props.entity) {
|
||||||
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
|
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
|
||||||
|
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||||
|
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/content`, 'success', t('Campaign saved'));
|
||||||
} else {
|
} else {
|
||||||
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
|
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
|
||||||
}
|
}
|
||||||
|
@ -494,6 +484,14 @@ export default class CUD extends Component {
|
||||||
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
|
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let saveButtonLabel;
|
||||||
|
if (isEdit) {
|
||||||
|
saveButtonLabel = t('Save');
|
||||||
|
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||||
|
saveButtonLabel = t('Save and edit content');
|
||||||
|
} else {
|
||||||
|
saveButtonLabel = t('Save and edit campaign');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -545,7 +543,7 @@ export default class CUD extends Component {
|
||||||
{templateEdit}
|
{templateEdit}
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={isEdit ? t('Save') : t('Save and edit campaign')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={saveButtonLabel}/>
|
||||||
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.entity.id}/delete`}/> }
|
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.entity.id}/delete`}/> }
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -97,6 +97,7 @@ export default class List extends Component {
|
||||||
actions: data => {
|
actions: data => {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const perms = data[9];
|
const perms = data[9];
|
||||||
|
const campaignType = data[3];
|
||||||
const campaignSource = data[6];
|
const campaignSource = data[6];
|
||||||
|
|
||||||
if (perms.includes('edit')) {
|
if (perms.includes('edit')) {
|
||||||
|
@ -127,6 +128,13 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (campaignType === CampaignType.TRIGGERED && perms.includes('viewTriggers')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="flash" title={t('Triggers')}/>,
|
||||||
|
link: `/campaigns/${data[0]}/triggers`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (perms.includes('share')) {
|
if (perms.includes('share')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||||
|
|
|
@ -7,7 +7,12 @@ import Content from './Content';
|
||||||
import CampaignsList from './List';
|
import CampaignsList from './List';
|
||||||
import Share from '../shares/Share';
|
import Share from '../shares/Share';
|
||||||
import Files from "../lib/files";
|
import Files from "../lib/files";
|
||||||
import {CampaignSource, CampaignType} from "../../../shared/campaigns";
|
import {
|
||||||
|
CampaignSource,
|
||||||
|
CampaignType
|
||||||
|
} from "../../../shared/campaigns";
|
||||||
|
import TriggersCUD from './triggers/CUD';
|
||||||
|
import TriggersList from './triggers/List';
|
||||||
|
|
||||||
|
|
||||||
function getMenus(t) {
|
function getMenus(t) {
|
||||||
|
@ -51,6 +56,32 @@ function getMenus(t) {
|
||||||
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
|
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
|
||||||
panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to resulting eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
|
panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to resulting eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
|
||||||
},
|
},
|
||||||
|
triggers: {
|
||||||
|
title: t('Triggers'),
|
||||||
|
link: params => `/campaigns/${params.campaignId}/triggers/`,
|
||||||
|
visible: resolved => resolved.campaign.type === CampaignType.TRIGGERED && resolved.campaign.permissions.includes('viewTriggers'),
|
||||||
|
panelRender: props => <TriggersList campaign={props.resolved.campaign} />,
|
||||||
|
children: {
|
||||||
|
':triggerId([0-9]+)': {
|
||||||
|
title: resolved => t('Trigger "{{name}}"', {name: resolved.trigger.name}),
|
||||||
|
resolve: {
|
||||||
|
trigger: params => `rest/triggers/${params.campaignId}/${params.triggerId}`,
|
||||||
|
},
|
||||||
|
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
|
||||||
|
navs: {
|
||||||
|
':action(edit|delete)': {
|
||||||
|
title: t('Edit'),
|
||||||
|
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
|
||||||
|
panelRender: props => <TriggersCUD action={props.match.params.action} entity={props.resolved.trigger} campaign={props.resolved.campaign} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
title: t('Create'),
|
||||||
|
panelRender: props => <TriggersCUD action="create" campaign={props.resolved.campaign} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/campaigns/${params.campaignId}/share`,
|
link: params => `/campaigns/${params.campaignId}/share`,
|
||||||
|
|
237
client/src/campaigns/triggers/CUD.js
Normal file
237
client/src/campaigns/triggers/CUD.js
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {translate} from 'react-i18next';
|
||||||
|
import {
|
||||||
|
NavButton,
|
||||||
|
requiresAuthenticatedUser,
|
||||||
|
Title,
|
||||||
|
withPageHelpers
|
||||||
|
} from '../../lib/page';
|
||||||
|
import {
|
||||||
|
AlignedRow,
|
||||||
|
Button,
|
||||||
|
ButtonRow,
|
||||||
|
CheckBox,
|
||||||
|
Dropdown,
|
||||||
|
Form,
|
||||||
|
FormSendMethod,
|
||||||
|
InputField,
|
||||||
|
TableSelect,
|
||||||
|
TextArea,
|
||||||
|
withForm
|
||||||
|
} from '../../lib/form';
|
||||||
|
import {withErrorHandling} from '../../lib/error-handling';
|
||||||
|
import {DeleteModalDialog} from "../../lib/modals";
|
||||||
|
import {getTriggerTypes} from './helpers';
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Event
|
||||||
|
} from '../../../../shared/triggers';
|
||||||
|
import moment from 'moment';
|
||||||
|
import {getCampaignTypeLabels} from "../helpers";
|
||||||
|
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withForm
|
||||||
|
@withPageHelpers
|
||||||
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
|
export default class CUD extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
|
||||||
|
this.campaignTypes = getCampaignTypeLabels(props.t);
|
||||||
|
|
||||||
|
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
|
||||||
|
this.entityLabels = entityLabels;
|
||||||
|
|
||||||
|
this.entityOptions = [
|
||||||
|
{key: Entity.SUBSCRIPTION, label: entityLabels[Entity.SUBSCRIPTION]},
|
||||||
|
{key: Entity.CAMPAIGN, label: entityLabels[Entity.CAMPAIGN]}
|
||||||
|
];
|
||||||
|
|
||||||
|
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
|
||||||
|
const CampaignEvent = Event[Entity.CAMPAIGN];
|
||||||
|
this.eventOptions = {
|
||||||
|
[Entity.SUBSCRIPTION]: [
|
||||||
|
{key: SubscriptionEvent.CREATED, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.CREATED]},
|
||||||
|
{key: SubscriptionEvent.LATEST_OPEN, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.LATEST_OPEN]},
|
||||||
|
{key: SubscriptionEvent.LATEST_CLICK, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.LATEST_CLICK]}
|
||||||
|
],
|
||||||
|
[Entity.CAMPAIGN]: [
|
||||||
|
{key: CampaignEvent.DELIVERED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.DELIVERED]},
|
||||||
|
{key: CampaignEvent.OPENED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.OPENED]},
|
||||||
|
{key: CampaignEvent.CLICKED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.CLICKED]},
|
||||||
|
{key: CampaignEvent.NOT_OPENED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.NOT_OPENED]},
|
||||||
|
{key: CampaignEvent.NOT_CLICKED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.NOT_CLICKED]}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.initForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
action: PropTypes.string.isRequired,
|
||||||
|
campaign: PropTypes.object,
|
||||||
|
entity: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.entity) {
|
||||||
|
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||||
|
data.daysAfter = (Math.round(data.seconds_after / (3600 * 24))).toString();
|
||||||
|
|
||||||
|
if (data.entity === Entity.SUBSCRIPTION) {
|
||||||
|
data.subscriptionEvent = data.event;
|
||||||
|
} else {
|
||||||
|
data.subscriptionEvent = Event[Entity.SUBSCRIPTION].CREATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.entity === Entity.CAMPAIGN) {
|
||||||
|
data.campaignEvent = data.event;
|
||||||
|
} else {
|
||||||
|
data.campaignEvent = Event[Entity.CAMPAIGN].DELIVERED;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.populateFormValues({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
entity: Entity.SUBSCRIPTION,
|
||||||
|
subscriptionEvent: Event[Entity.SUBSCRIPTION].CREATED,
|
||||||
|
campaignEvent: Event[Entity.CAMPAIGN].DELIVERED,
|
||||||
|
daysAfter: '',
|
||||||
|
enabled: true,
|
||||||
|
source_campaign: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localValidateFormValues(state) {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
const entityKey = state.getIn(['entity', 'value']);
|
||||||
|
|
||||||
|
if (!state.getIn(['name', 'value'])) {
|
||||||
|
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['name', 'error'], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysAfter = state.getIn(['daysAfter', 'value']).trim();
|
||||||
|
if (daysAfter === '') {
|
||||||
|
state.setIn(['daysAfter', 'error'], t('Values must not be empty'));
|
||||||
|
} else if (isNaN(daysAfter) || Number.parseInt(daysAfter) < 0) {
|
||||||
|
state.setIn(['daysAfter', 'error'], t('Value must be a non-negative number'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['daysAfter', 'error'], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityKey === Entity.CAMPAIGN && !state.getIn(['source_campaign', 'value'])) {
|
||||||
|
state.setIn(['source_campaign', 'error'], t('Source campaign must not be empty'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['source_campaign', 'error'], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitHandler() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
let sendMethod, url;
|
||||||
|
if (this.props.entity) {
|
||||||
|
sendMethod = FormSendMethod.PUT;
|
||||||
|
url = `rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`
|
||||||
|
} else {
|
||||||
|
sendMethod = FormSendMethod.POST;
|
||||||
|
url = `rest/triggers/${this.props.campaign.id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.disableForm();
|
||||||
|
this.setFormStatusMessage('info', t('Saving ...'));
|
||||||
|
|
||||||
|
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||||
|
data.seconds_after = Number.parseInt(data.daysAfter) * 3600 * 24;
|
||||||
|
|
||||||
|
if (data.entity === Entity.SUBSCRIPTION) {
|
||||||
|
data.event = data.subscriptionEvent;
|
||||||
|
} else if (data.entity === Entity.CAMPAIGN) {
|
||||||
|
data.event = data.campaignEvent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (submitSuccessful) {
|
||||||
|
this.navigateToWithFlashMessage(`/campaigns/${this.props.campaign.id}/triggers`, 'success', t('Trigger saved'));
|
||||||
|
} else {
|
||||||
|
this.enableForm();
|
||||||
|
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
const isEdit = !!this.props.entity;
|
||||||
|
|
||||||
|
const entityKey = this.getFormValue('entity');
|
||||||
|
|
||||||
|
const campaignsColumns = [
|
||||||
|
{ data: 1, title: t('Name') },
|
||||||
|
{ data: 2, title: t('Description') },
|
||||||
|
{ data: 3, title: t('Type'), render: data => this.campaignTypes[data] },
|
||||||
|
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
|
||||||
|
{ data: 5, title: t('Namespace') }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isEdit &&
|
||||||
|
<DeleteModalDialog
|
||||||
|
stateOwner={this}
|
||||||
|
visible={this.props.action === 'delete'}
|
||||||
|
deleteUrl={`rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`}
|
||||||
|
cudUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`}
|
||||||
|
listUrl={`/campaigns/${this.props.campaign.id}/triggers`}
|
||||||
|
deletingMsg={t('Deleting trigger ...')}
|
||||||
|
deletedMsg={t('Trigger deleted')}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Title>{isEdit ? t('Edit Trigger') : t('Create Trigger')}</Title>
|
||||||
|
|
||||||
|
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||||
|
<InputField id="name" label={t('Name')}/>
|
||||||
|
<TextArea id="description" label={t('Description')}/>
|
||||||
|
|
||||||
|
<Dropdown id="entity" label={t('Entity')} options={this.entityOptions} help={t('Select the type of the trigger rule.')}/>
|
||||||
|
|
||||||
|
<InputField id="daysAfter" label={t('Trigger fires')}/>
|
||||||
|
|
||||||
|
<AlignedRow>days after:</AlignedRow>
|
||||||
|
|
||||||
|
{entityKey === Entity.SUBSCRIPTION && <Dropdown id="subscriptionEvent" label={t('Event')} options={this.eventOptions[Entity.SUBSCRIPTION]} help={t('Select the event that triggers sending the campaign.')}/>}
|
||||||
|
|
||||||
|
{entityKey === Entity.CAMPAIGN && <Dropdown id="campaignEvent" label={t('Event')} options={this.eventOptions[Entity.CAMPAIGN]} help={t('Select the event that triggers sending the campaign.')}/>}
|
||||||
|
|
||||||
|
{entityKey === Entity.CAMPAIGN &&
|
||||||
|
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${this.props.campaign.list}`} columns={campaignsColumns} selectionLabelIndex={1} />
|
||||||
|
}
|
||||||
|
|
||||||
|
<CheckBox id="enabled" text={t('Enabled')}/>
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
|
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/delete`}/>}
|
||||||
|
</ButtonRow>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
81
client/src/campaigns/triggers/List.js
Normal file
81
client/src/campaigns/triggers/List.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {translate} from 'react-i18next';
|
||||||
|
import {
|
||||||
|
NavButton,
|
||||||
|
requiresAuthenticatedUser,
|
||||||
|
Title,
|
||||||
|
Toolbar,
|
||||||
|
withPageHelpers
|
||||||
|
} from '../../lib/page';
|
||||||
|
import {withErrorHandling} from '../../lib/error-handling';
|
||||||
|
import {Table} from '../../lib/table';
|
||||||
|
import {getTriggerTypes} from './helpers';
|
||||||
|
import {Icon} from "../../lib/bootstrap-components";
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withPageHelpers
|
||||||
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
|
export default class List extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
|
||||||
|
this.entityLabels = entityLabels;
|
||||||
|
this.eventLabels = eventLabels;
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
campaign: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ data: 1, title: t('Name') },
|
||||||
|
{ data: 2, title: t('Description') },
|
||||||
|
{ data: 3, title: t('List') },
|
||||||
|
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
|
||||||
|
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
|
||||||
|
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
|
||||||
|
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
|
||||||
|
{
|
||||||
|
actions: data => {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (this.props.campaign.permissions.includes('manageTriggers')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
|
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.props.campaign.permissions.includes('manageTriggers') &&
|
||||||
|
<Toolbar>
|
||||||
|
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
|
||||||
|
</Toolbar>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Title>{t('Triggers')}</Title>
|
||||||
|
|
||||||
|
<Table withHeader dataUrl={`rest/triggers-by-campaign-table/${this.props.campaign.id}`} columns={columns} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
client/src/campaigns/triggers/helpers.js
Normal file
35
client/src/campaigns/triggers/helpers.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {Entity, Event} from '../../../../shared/triggers';
|
||||||
|
|
||||||
|
export function getTriggerTypes(t) {
|
||||||
|
|
||||||
|
const entityLabels = {
|
||||||
|
[Entity.SUBSCRIPTION]: t('Subscription'),
|
||||||
|
[Entity.CAMPAIGN]: t('Campaign')
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
|
||||||
|
const CampaignEvent = Event[Entity.CAMPAIGN];
|
||||||
|
|
||||||
|
const eventLabels = {
|
||||||
|
[Entity.SUBSCRIPTION]: {
|
||||||
|
[SubscriptionEvent.CREATED]: t('Created'),
|
||||||
|
[SubscriptionEvent.LATEST_OPEN]: t('Latest open'),
|
||||||
|
[SubscriptionEvent.LATEST_CLICK]: t('Latest click')
|
||||||
|
},
|
||||||
|
[Entity.CAMPAIGN]: {
|
||||||
|
[CampaignEvent.DELIVERED]: t('Delivered'),
|
||||||
|
[CampaignEvent.OPENED]: t('Opened'),
|
||||||
|
[CampaignEvent.CLICKED]: t('Clicked'),
|
||||||
|
[CampaignEvent.NOT_OPENED]: t('Not opened'),
|
||||||
|
[CampaignEvent.NOT_CLICKED]: t('Not clicked')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
entityLabels,
|
||||||
|
eventLabels
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
|
||||||
import mailtrainConfig from "mailtrainConfig";
|
import mailtrainConfig from "mailtrainConfig";
|
||||||
|
|
||||||
let restrictedAccessToken = 'ANONYMOUS';
|
let restrictedAccessToken = anonymousRestrictedAccessToken;
|
||||||
|
|
||||||
function setRestrictedAccessToken(token) {
|
function setRestrictedAccessToken(token) {
|
||||||
restrictedAccessToken = token;
|
restrictedAccessToken = token;
|
||||||
|
@ -28,7 +29,7 @@ function getBaseDir() {
|
||||||
if (mailtrainConfig.trusted) {
|
if (mailtrainConfig.trusted) {
|
||||||
return mailtrainConfig.trustedUrlBaseDir;
|
return mailtrainConfig.trustedUrlBaseDir;
|
||||||
} else {
|
} else {
|
||||||
return mailtrainConfig.sandboxUrlBaseDir + 'ANONYMOUS';
|
return mailtrainConfig.sandboxUrlBaseDir + anonymousRestrictedAccessToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default class List extends Component {
|
||||||
data: 1,
|
data: 1,
|
||||||
title: t('Name'),
|
title: t('Name'),
|
||||||
actions: data => {
|
actions: data => {
|
||||||
const perms = data[6];
|
const perms = data[7];
|
||||||
if (perms.includes('viewSubscriptions')) {
|
if (perms.includes('viewSubscriptions')) {
|
||||||
return [{label: data[1], link: `/lists/${data[0]}/subscriptions`}];
|
return [{label: data[1], link: `/lists/${data[0]}/subscriptions`}];
|
||||||
} else {
|
} else {
|
||||||
|
@ -62,7 +62,9 @@ export default class List extends Component {
|
||||||
{
|
{
|
||||||
actions: data => {
|
actions: data => {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const perms = data[6];
|
const triggersCount = data[6];
|
||||||
|
const perms = data[7];
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
if (perms.includes('viewSubscriptions')) {
|
if (perms.includes('viewSubscriptions')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
|
@ -92,6 +94,13 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (triggersCount > 0) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="flash" title={t('Triggers')}/>,
|
||||||
|
link: `/lists/${data[0]}/triggers`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (perms.includes('share')) {
|
if (perms.includes('share')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||||
|
|
75
client/src/lists/TriggersList.js
Normal file
75
client/src/lists/TriggersList.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {translate} from 'react-i18next';
|
||||||
|
import {
|
||||||
|
requiresAuthenticatedUser,
|
||||||
|
Title,
|
||||||
|
withPageHelpers
|
||||||
|
} from '../lib/page';
|
||||||
|
import {withErrorHandling} from '../lib/error-handling';
|
||||||
|
import {Table} from '../lib/table';
|
||||||
|
import {getTriggerTypes} from '../campaigns/triggers/helpers';
|
||||||
|
import {Icon} from "../lib/bootstrap-components";
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withPageHelpers
|
||||||
|
@withErrorHandling
|
||||||
|
@requiresAuthenticatedUser
|
||||||
|
export default class List extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
|
||||||
|
this.entityLabels = entityLabels;
|
||||||
|
this.eventLabels = eventLabels;
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
list: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ data: 1, title: t('Name') },
|
||||||
|
{ data: 2, title: t('Description') },
|
||||||
|
{ data: 3, title: t('Campaign') },
|
||||||
|
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
|
||||||
|
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
|
||||||
|
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
|
||||||
|
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
|
||||||
|
{
|
||||||
|
actions: data => {
|
||||||
|
const actions = [];
|
||||||
|
const perms = data[9];
|
||||||
|
const campaignId = data[8];
|
||||||
|
|
||||||
|
if (perms.includes('manageTriggers')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
|
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title>{t('Triggers')}</Title>
|
||||||
|
|
||||||
|
<Table withHeader dataUrl={`rest/triggers-by-list-table/${this.props.list.id}`} columns={columns} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import SubscriptionsCUD from './subscriptions/CUD';
|
||||||
import SegmentsList from './segments/List';
|
import SegmentsList from './segments/List';
|
||||||
import SegmentsCUD from './segments/CUD';
|
import SegmentsCUD from './segments/CUD';
|
||||||
import Share from '../shares/Share';
|
import Share from '../shares/Share';
|
||||||
|
import TriggersList from './TriggersList';
|
||||||
|
|
||||||
function getMenus(t) {
|
function getMenus(t) {
|
||||||
return {
|
return {
|
||||||
|
@ -127,6 +127,11 @@ function getMenus(t) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
triggers: {
|
||||||
|
title: t('Triggers'),
|
||||||
|
link: params => `/lists/${params.listId}/triggers`,
|
||||||
|
panelRender: props => <TriggersList list={props.resolved.list} />
|
||||||
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/lists/${params.listId}/share`,
|
link: params => `/lists/${params.listId}/share`,
|
||||||
|
|
|
@ -94,17 +94,17 @@ export default class CUD extends Component {
|
||||||
localValidateFormValues(state) {
|
localValidateFormValues(state) {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
|
for (const key of state.keys()) {
|
||||||
|
state.setIn([key, 'error'], null);
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.getIn(['name', 'value'])) {
|
if (!state.getIn(['name', 'value'])) {
|
||||||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||||
} else {
|
|
||||||
state.setIn(['name', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeKey = state.getIn(['type', 'value']);
|
const typeKey = state.getIn(['type', 'value']);
|
||||||
if (!typeKey) {
|
if (!typeKey) {
|
||||||
state.setIn(['type', 'error'], t('Type must be selected'));
|
state.setIn(['type', 'error'], t('Type must be selected'));
|
||||||
} else {
|
|
||||||
state.setIn(['type', 'error'], null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validateNamespace(t, state);
|
validateNamespace(t, state);
|
||||||
|
|
|
@ -112,14 +112,12 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
|
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
|
||||||
if (!mosaicoTemplate) {
|
if (!mosaicoTemplate) {
|
||||||
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
|
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
|
||||||
} else {
|
|
||||||
state.setIn([prefix + 'mosaicoTemplate', 'error'], null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates.map(([key, label]) => ({key, label}));
|
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates;
|
||||||
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates);
|
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates.map(({key, label}) => ([key, label])));
|
||||||
|
|
||||||
templateTypes.mosaicoWithFsTemplate = {
|
templateTypes.mosaicoWithFsTemplate = {
|
||||||
typeName: t('Mosaico with predefined templates'),
|
typeName: t('Mosaico with predefined templates'),
|
||||||
|
@ -151,7 +149,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initData: () => ({
|
initData: () => ({
|
||||||
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0][0],
|
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0].key,
|
||||||
[prefix + 'mosaicoData']: {}
|
[prefix + 'mosaicoData']: {}
|
||||||
}),
|
}),
|
||||||
afterLoad: data => {
|
afterLoad: data => {
|
||||||
|
|
|
@ -1,322 +0,0 @@
|
||||||
# This file is the default config file for Mailtrain. To use a environment specific
|
|
||||||
# configuration add new file {ENV}.{ext} (eg. "production.toml") to the same folder.
|
|
||||||
# {ENV} is defined by NODE_ENV environment variable.
|
|
||||||
#
|
|
||||||
# Do not modify this file directly, otherwise you might lose your modifications when upgrading
|
|
||||||
#
|
|
||||||
# You should only define the options you want to change in your additional config file.
|
|
||||||
# For example if the only thing you want to change is the port number for the www server
|
|
||||||
# then your additional config file should look like this:
|
|
||||||
# # production.toml
|
|
||||||
# [www]
|
|
||||||
# port=80
|
|
||||||
# or if you want to use Javascript instead of TOML then the same file could look like this:
|
|
||||||
# // production.js
|
|
||||||
# module.exports = {
|
|
||||||
# www: {
|
|
||||||
# port: 80
|
|
||||||
# }
|
|
||||||
# };
|
|
||||||
|
|
||||||
# Process title visible in monitoring logs and process listing
|
|
||||||
title="mailtrain"
|
|
||||||
|
|
||||||
# Enabled HTML editors
|
|
||||||
editors=["ckeditor", "codeeditor", "mosaico", "mosaicoWithFsTemplate"]
|
|
||||||
|
|
||||||
# Default language to use
|
|
||||||
language="en"
|
|
||||||
|
|
||||||
# Inject custom scripts in subscription/layout.mjml.hbs
|
|
||||||
# customSubscriptionScripts=["/custom/hello-world.js"]
|
|
||||||
|
|
||||||
# If you start out as a root user (eg. if you want to use ports lower than 1000)
|
|
||||||
# then you can downgrade the user once all services are up and running
|
|
||||||
#user="mailtrain"
|
|
||||||
#group="mailtrain"
|
|
||||||
|
|
||||||
# If Mailtrain is started as root, "Reports" feature drops the privileges of script generating the report to disallow
|
|
||||||
# any modifications of Mailtrain code and even prohibits reading the production configuration (which contains the MySQL
|
|
||||||
# password for read/write operations). The roUser/roGroup determines the user to be used
|
|
||||||
#roUser="nobody"
|
|
||||||
#roGroup="nogroup"
|
|
||||||
|
|
||||||
[log]
|
|
||||||
# silly|verbose|info|http|warn|error|silent
|
|
||||||
level="verbose"
|
|
||||||
|
|
||||||
[www]
|
|
||||||
# HTTP port to listen on
|
|
||||||
port=3000
|
|
||||||
# HTTP port to listen on for sandboxed requests
|
|
||||||
sandboxPort=8081
|
|
||||||
# HTTP interface to listen on
|
|
||||||
host="0.0.0.0"
|
|
||||||
# URL base for trusted urls. It must be absolute (starting with http:// or https://). If Mailtrain is served on
|
|
||||||
# a non-standard port (e.g. 3000), the URL must also specify the port.
|
|
||||||
trustedUrlBase="http://localhost:3000"
|
|
||||||
# URL base for sandbox urls. It must be absolute (starting with http:// or https://) and contain the sandbox port.
|
|
||||||
sandboxUrlBase="http://localhost:8081"
|
|
||||||
|
|
||||||
# Secret for signing the session ID cookie
|
|
||||||
secret="a cat"
|
|
||||||
# Session length in seconds when "remember me" is checked
|
|
||||||
remember=2592000 # 30 days
|
|
||||||
# logger interface for expressjs morgan
|
|
||||||
log="dev"
|
|
||||||
# Is the server behind a proxy? true/false
|
|
||||||
# Set this to true if you are serving Mailtrain as a virtual domain through Nginx or Apache
|
|
||||||
proxy=false
|
|
||||||
# maximum POST body size
|
|
||||||
postSize="2MB"
|
|
||||||
|
|
||||||
[mysql]
|
|
||||||
host="localhost"
|
|
||||||
user="mailtrain"
|
|
||||||
password="mailtrain"
|
|
||||||
database="mailtrain"
|
|
||||||
# Some installations, eg. MAMP can use a different port (8889)
|
|
||||||
# MAMP users should also turn on "Allow network access to MySQL" otherwise MySQL might not be accessible
|
|
||||||
port=3306
|
|
||||||
charset="utf8mb4"
|
|
||||||
# The timezone configured on the MySQL server. This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM
|
|
||||||
timezone="local"
|
|
||||||
|
|
||||||
[redis]
|
|
||||||
# enable to use Redis session cache or disable if Redis is not installed
|
|
||||||
enabled=false
|
|
||||||
host="localhost"
|
|
||||||
port=6379
|
|
||||||
db=5
|
|
||||||
# Uncomment if your Redis installation requires a password
|
|
||||||
#password=""
|
|
||||||
|
|
||||||
[verp]
|
|
||||||
# Enable to start an MX server that detects bounced messages using VERP
|
|
||||||
# In most cases you do not want to use it
|
|
||||||
# Requires root privileges
|
|
||||||
enabled=false
|
|
||||||
port=2525
|
|
||||||
host="0.0.0.0"
|
|
||||||
# With DMARC, the Return-Path and From address must match the same domain.
|
|
||||||
# By default we get around this by using the VERP address in the Sender header,
|
|
||||||
# with the side effect that some email clients diplay an ugly "on behalf of" message.
|
|
||||||
# You can safely disable this Sender header if you're not using DMARC or your
|
|
||||||
# VERP hostname is in the same domain as the From address.
|
|
||||||
# disablesenderheader=true
|
|
||||||
|
|
||||||
[ldap]
|
|
||||||
# enable to use ldap user backend
|
|
||||||
enabled=false
|
|
||||||
# method is ldapjs or ldapauth - it chooses the library to be used. If not given, it chooses the one present.
|
|
||||||
# method="ldapjs"
|
|
||||||
host="localhost"
|
|
||||||
port=3002
|
|
||||||
baseDN="ou=users,dc=company"
|
|
||||||
filter="(|(username={{username}})(mail={{username}}))"
|
|
||||||
# Username field in LDAP (uid/cn/username)
|
|
||||||
uidTag="username"
|
|
||||||
# nameTag identifies the attribute to be used for user's full name
|
|
||||||
nameTag="username"
|
|
||||||
passwordresetlink=""
|
|
||||||
newUserRole="master"
|
|
||||||
# Global namespace id
|
|
||||||
newUserNamespaceId=1
|
|
||||||
# Use a different user to bind LDAP (final bind DN will be: {{uidTag}}={{bindUser}},{{baseDN}})
|
|
||||||
bindUser="name@company.net"
|
|
||||||
bindPassword="mySecretPassword"
|
|
||||||
|
|
||||||
[postfixbounce]
|
|
||||||
# Enable to allow writing Postfix bounce log to Mailtrain listener
|
|
||||||
# If enabled, tail mail.log to Mailtrain with the following command:
|
|
||||||
# tail -f -n +0 /var/log/mail.log | nc localhost 5699 -
|
|
||||||
enabled=false
|
|
||||||
port=5699
|
|
||||||
# allow connections from localhost only
|
|
||||||
host="127.0.0.1"
|
|
||||||
|
|
||||||
# extra options for nodemailer
|
|
||||||
[nodemailer]
|
|
||||||
#textEncoding="base64"
|
|
||||||
|
|
||||||
[queue]
|
|
||||||
# How many parallel sender processes to spawn
|
|
||||||
# You can use more than 1 process only if you have Redis enabled
|
|
||||||
processes=1
|
|
||||||
|
|
||||||
[cors]
|
|
||||||
# Allow subscription widgets to be embedded
|
|
||||||
# origins=['https://www.example.com']
|
|
||||||
|
|
||||||
[mosaico]
|
|
||||||
# Installed templates
|
|
||||||
fsTemplates=[["versafix-1", "Versafix One"]]
|
|
||||||
# Inject custom scripts
|
|
||||||
# customscripts=["/mosaico/custom/my-mosaico-plugin.js"]
|
|
||||||
|
|
||||||
[grapejs]
|
|
||||||
# Installed templates
|
|
||||||
templates=[
|
|
||||||
["demo", "HTML Template"],
|
|
||||||
["aves", "MJML Template"]
|
|
||||||
]
|
|
||||||
|
|
||||||
[reports]
|
|
||||||
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
|
|
||||||
# properly protected.
|
|
||||||
# Reports rely on custom user defined Javascript snippets defined in the report template. The snippets are run on the
|
|
||||||
# server when generating a report. As these snippets are stored in the DB, they pose a security risk because they can
|
|
||||||
# help gaining access to the server if the DB cannot
|
|
||||||
# be properly protected (e.g. if it is shared with another application with security weaknesses).
|
|
||||||
# Mailtrain mitigates this problem by running the custom Javascript snippets in a chrooted environment and under a
|
|
||||||
# DB user that cannot modify the database (see userRO in [mysql] above). However the chrooted environment is available
|
|
||||||
# only if Mailtrain is started as root. The chrooted environment still does not prevent the custom JS script in
|
|
||||||
# performing network operations and in generating XSS attacks as part of the report.
|
|
||||||
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
|
|
||||||
# then it's safer to switch off the reporting functionality below.
|
|
||||||
enabled=false
|
|
||||||
|
|
||||||
[testserver]
|
|
||||||
# Starts a vanity server that redirects all mail to /dev/null
|
|
||||||
# Mostly needed for local development
|
|
||||||
enabled=false
|
|
||||||
port=5587
|
|
||||||
mailboxserverport=3001
|
|
||||||
host="0.0.0.0"
|
|
||||||
username="testuser"
|
|
||||||
password="testpass"
|
|
||||||
logger=false
|
|
||||||
|
|
||||||
[seleniumwebdriver]
|
|
||||||
browser="phantomjs"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[roles.global.master]
|
|
||||||
name="Master"
|
|
||||||
admin=true
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["rebuildPermissions", "createJavascriptWithROAccess", "manageBlacklist", "manageSettings"]
|
|
||||||
rootNamespaceRole="master"
|
|
||||||
|
|
||||||
[roles.namespace.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "createCampaign", "manageUsers"]
|
|
||||||
|
|
||||||
[roles.namespace.master.children]
|
|
||||||
sendConfiguration=["viewPublic", "viewPrivate", "edit", "delete", "share", "sendWithoutOverrides", "sendWithAllowedOverrides", "sendWithAnyOverrides"]
|
|
||||||
list=["view", "edit", "delete", "share", "viewFields", "manageFields", "viewSubscriptions", "manageSubscriptions", "viewSegments", "manageSegments"]
|
|
||||||
customForm=["view", "edit", "delete", "share"]
|
|
||||||
campaign=["view", "edit", "delete", "share", "viewFiles", "manageFiles", "viewAttachments", "manageAttachments", "viewTriggers", "manageTriggers", "send", "viewStats"]
|
|
||||||
template=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
|
||||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
|
||||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
|
||||||
mosaicoTemplate=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
|
||||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "createCampaign", "manageUsers"]
|
|
||||||
|
|
||||||
[roles.sendConfiguration.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["viewPublic", "viewPrivate", "edit", "delete", "share", "sendWithoutOverrides", "sendWithAllowedOverrides", "sendWithAnyOverrides"]
|
|
||||||
|
|
||||||
[roles.list.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share", "viewFields", "manageFields", "viewSubscriptions", "manageSubscriptions", "viewSegments", "manageSegments"]
|
|
||||||
|
|
||||||
[roles.customForm.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share"]
|
|
||||||
|
|
||||||
[roles.campaign.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles", "viewAttachments", "manageAttachments", "viewTriggers", "manageTriggers", "send", "viewStats"]
|
|
||||||
|
|
||||||
[roles.template.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
|
||||||
|
|
||||||
[roles.report.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
|
||||||
|
|
||||||
[roles.reportTemplate.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share", "execute"]
|
|
||||||
|
|
||||||
[roles.mosaicoTemplate.master]
|
|
||||||
name="Master"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[roles.global.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
ownNamespaceRole="editor"
|
|
||||||
|
|
||||||
[roles.namespace.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=["view", "edit", "delete"]
|
|
||||||
|
|
||||||
[roles.namespace.editor.children]
|
|
||||||
sendConfiguration=[]
|
|
||||||
list=[]
|
|
||||||
customForm=[]
|
|
||||||
campaign=[]
|
|
||||||
template=[]
|
|
||||||
report=[]
|
|
||||||
reportTemplate=[]
|
|
||||||
namespace=["view", "edit", "delete"]
|
|
||||||
mosaicoTemplate=[]
|
|
||||||
|
|
||||||
[roles.sendConfiguration.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
|
|
||||||
[roles.list.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
|
|
||||||
[roles.customForm.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="All permissions"
|
|
||||||
permissions=[]
|
|
||||||
|
|
||||||
[roles.campaign.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
|
|
||||||
[roles.template.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
|
|
||||||
[roles.report.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
|
|
||||||
[roles.reportTemplate.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
|
|
||||||
[roles.mosaicoTemplate.editor]
|
|
||||||
name="Editor"
|
|
||||||
description="XXX"
|
|
||||||
permissions=[]
|
|
||||||
|
|
268
config/default.yaml
Normal file
268
config/default.yaml
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
# This file is the default config file for Mailtrain. To use a environment specific
|
||||||
|
# configuration add new file {ENV}.{ext} (eg. production.yaml) to the same folder.
|
||||||
|
# {ENV} is defined by NODE_ENV environment variable.
|
||||||
|
#
|
||||||
|
# Do not modify this file directly, otherwise you might lose your modifications when upgrading
|
||||||
|
#
|
||||||
|
# You should only define the options you want to change in your additional config file.
|
||||||
|
# For example if the only thing you want to change is the port number for the www server
|
||||||
|
# then your additional config file should look like this:
|
||||||
|
# # production.yaml
|
||||||
|
# www:
|
||||||
|
# port: 80
|
||||||
|
|
||||||
|
# Process title visible in monitoring logs and process listing
|
||||||
|
title: mailtrain
|
||||||
|
|
||||||
|
# Enabled HTML editors
|
||||||
|
editors:
|
||||||
|
- ckeditor
|
||||||
|
- codeeditor
|
||||||
|
- mosaico
|
||||||
|
- mosaicoWithFsTemplate
|
||||||
|
|
||||||
|
# Default language to use
|
||||||
|
language: en
|
||||||
|
|
||||||
|
# Inject custom scripts in subscription/layout.mjml.hbs
|
||||||
|
# customSubscriptionScripts: [/custom/hello-world.js]
|
||||||
|
|
||||||
|
# If you start out as a root user (eg. if you want to use ports lower than 1000)
|
||||||
|
# then you can downgrade the user once all services are up and running
|
||||||
|
#user: mailtrain
|
||||||
|
#group: mailtrain
|
||||||
|
|
||||||
|
# If Mailtrain is started as root, Reports feature drops the privileges of script generating the report to disallow
|
||||||
|
# any modifications of Mailtrain code and even prohibits reading the production configuration (which contains the MySQL
|
||||||
|
# password for read/write operations). The roUser/roGroup determines the user to be used
|
||||||
|
#roUser: nobody
|
||||||
|
#roGroup: nogroup
|
||||||
|
|
||||||
|
log:
|
||||||
|
# silly|verbose|info|http|warn|error|silent
|
||||||
|
level: verbose
|
||||||
|
|
||||||
|
www:
|
||||||
|
# HTTP port to listen on
|
||||||
|
port: 3000
|
||||||
|
# HTTP port to listen on for sandboxed requests
|
||||||
|
sandboxPort: 8081
|
||||||
|
# HTTP interface to listen on
|
||||||
|
host: 0.0.0.0
|
||||||
|
# URL base for trusted urls. It must be absolute (starting with http:// or https://). If Mailtrain is served on
|
||||||
|
# a non-standard port (e.g. 3000), the URL must also specify the port.
|
||||||
|
trustedUrlBase: http://localhost:3000
|
||||||
|
# URL base for sandbox urls. It must be absolute (starting with http:// or https://) and contain the sandbox port.
|
||||||
|
sandboxUrlBase: http://localhost:8081
|
||||||
|
|
||||||
|
# Secret for signing the session ID cookie
|
||||||
|
secret: a cat
|
||||||
|
# Session length in seconds when remember me is checked
|
||||||
|
remember: 2592000 # 30 days
|
||||||
|
# logger interface for expressjs morgan
|
||||||
|
log: dev
|
||||||
|
# Is the server behind a proxy? true/false
|
||||||
|
# Set this to true if you are serving Mailtrain as a virtual domain through Nginx or Apache
|
||||||
|
proxy: false
|
||||||
|
# maximum POST body size
|
||||||
|
postSize: 2MB
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
user: mailtrain
|
||||||
|
password: mailtrain
|
||||||
|
database: mailtrain
|
||||||
|
# Some installations, eg. MAMP can use a different port (8889)
|
||||||
|
# MAMP users should also turn on Allow network access to MySQL otherwise MySQL might not be accessible
|
||||||
|
port: 3306
|
||||||
|
charset: utf8mb4
|
||||||
|
# The timezone configured on the MySQL server. This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM
|
||||||
|
timezone: local
|
||||||
|
|
||||||
|
redis:
|
||||||
|
# enable to use Redis session cache or disable if Redis is not installed
|
||||||
|
enabled: false
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
db: 5
|
||||||
|
# Uncomment if your Redis installation requires a password
|
||||||
|
#password:
|
||||||
|
|
||||||
|
verp:
|
||||||
|
# Enable to start an MX server that detects bounced messages using VERP
|
||||||
|
# In most cases you do not want to use it
|
||||||
|
# Requires root privileges
|
||||||
|
enabled: false
|
||||||
|
port: 2525
|
||||||
|
host: 0.0.0.0
|
||||||
|
# With DMARC, the Return-Path and From address must match the same domain.
|
||||||
|
# By default we get around this by using the VERP address in the Sender header,
|
||||||
|
# with the side effect that some email clients diplay an ugly on behalf of message.
|
||||||
|
# You can safely disable this Sender header if you're not using DMARC or your
|
||||||
|
# VERP hostname is in the same domain as the From address.
|
||||||
|
# disablesenderheader: true
|
||||||
|
|
||||||
|
ldap:
|
||||||
|
# enable to use ldap user backend
|
||||||
|
enabled: false
|
||||||
|
# method is ldapjs or ldapauth - it chooses the library to be used. If not given, it chooses the one present.
|
||||||
|
# method: ldapjs
|
||||||
|
host: localhost
|
||||||
|
port: 3002
|
||||||
|
baseDN: ou=users,dc=company
|
||||||
|
filter: (|(username={{username}})(mail={{username}}))
|
||||||
|
# Username field in LDAP (uid/cn/username)
|
||||||
|
uidTag: username
|
||||||
|
# nameTag identifies the attribute to be used for user's full name
|
||||||
|
nameTag: username
|
||||||
|
passwordresetlink:
|
||||||
|
newUserRole: master
|
||||||
|
# Global namespace id
|
||||||
|
newUserNamespaceId: 1
|
||||||
|
# Use a different user to bind LDAP (final bind DN will be: {{uidTag}}: {{bindUser}},{{baseDN}})
|
||||||
|
bindUser: name@company.net
|
||||||
|
bindPassword: mySecretPassword
|
||||||
|
|
||||||
|
postfixbounce:
|
||||||
|
# Enable to allow writing Postfix bounce log to Mailtrain listener
|
||||||
|
# If enabled, tail mail.log to Mailtrain with the following command:
|
||||||
|
# tail -f -n +0 /var/log/mail.log | nc localhost 5699 -
|
||||||
|
enabled: false
|
||||||
|
port: 5699
|
||||||
|
# allow connections from localhost only
|
||||||
|
host: 127.0.0.1
|
||||||
|
|
||||||
|
# extra options for nodemailer
|
||||||
|
nodemailer:
|
||||||
|
#textEncoding: base64
|
||||||
|
|
||||||
|
queue:
|
||||||
|
# How many parallel sender processes to spawn
|
||||||
|
# You can use more than 1 process only if you have Redis enabled
|
||||||
|
processes: 1
|
||||||
|
|
||||||
|
cors:
|
||||||
|
# Allow subscription widgets to be embedded
|
||||||
|
# origins: ['https://www.example.com']
|
||||||
|
|
||||||
|
mosaico:
|
||||||
|
# Installed templates
|
||||||
|
fsTemplates:
|
||||||
|
- key: versafix-1
|
||||||
|
label: Versafix One
|
||||||
|
# Inject custom scripts
|
||||||
|
# customscripts:
|
||||||
|
# - /mosaico/custom/my-mosaico-plugin.js
|
||||||
|
|
||||||
|
grapejs:
|
||||||
|
# Installed templates
|
||||||
|
templates:
|
||||||
|
- key: demo
|
||||||
|
label: HTML Template
|
||||||
|
- key: aves
|
||||||
|
label: MJML Template
|
||||||
|
|
||||||
|
reports:
|
||||||
|
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
|
||||||
|
# properly protected.
|
||||||
|
# Reports rely on custom user defined Javascript snippets defined in the report template. The snippets are run on the
|
||||||
|
# server when generating a report. As these snippets are stored in the DB, they pose a security risk because they can
|
||||||
|
# help gaining access to the server if the DB cannot
|
||||||
|
# be properly protected (e.g. if it is shared with another application with security weaknesses).
|
||||||
|
# Mailtrain mitigates this problem by running the custom Javascript snippets in a chrooted environment and under a
|
||||||
|
# DB user that cannot modify the database (see userRO in [mysql] above). However the chrooted environment is available
|
||||||
|
# only if Mailtrain is started as root. The chrooted environment still does not prevent the custom JS script in
|
||||||
|
# performing network operations and in generating XSS attacks as part of the report.
|
||||||
|
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
|
||||||
|
# then it's safer to switch off the reporting functionality below.
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
testserver:
|
||||||
|
# Starts a vanity server that redirects all mail to /dev/null
|
||||||
|
# Mostly needed for local development
|
||||||
|
enabled: false
|
||||||
|
port: 5587
|
||||||
|
mailboxserverport: 3001
|
||||||
|
host: 0.0.0.0
|
||||||
|
username: testuser
|
||||||
|
password: testpass
|
||||||
|
logger: false
|
||||||
|
|
||||||
|
seleniumwebdriver:
|
||||||
|
browser: phantomjs
|
||||||
|
|
||||||
|
|
||||||
|
roles:
|
||||||
|
global:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
admin: true
|
||||||
|
description: All permissions
|
||||||
|
permissions: [rebuildPermissions, createJavascriptWithROAccess, manageBlacklist, manageSettings]
|
||||||
|
rootNamespaceRole: master
|
||||||
|
|
||||||
|
namespace:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||||
|
children:
|
||||||
|
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
||||||
|
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments]
|
||||||
|
customForm: [view, edit, delete, share]
|
||||||
|
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats]
|
||||||
|
template: [view, edit, delete, share, viewFiles, manageFiles]
|
||||||
|
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||||
|
reportTemplate: [view, edit, delete, share, execute]
|
||||||
|
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
|
||||||
|
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||||
|
|
||||||
|
sendConfiguration:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
||||||
|
|
||||||
|
list:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments]
|
||||||
|
|
||||||
|
customForm:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share]
|
||||||
|
|
||||||
|
campaign:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats]
|
||||||
|
|
||||||
|
template:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share, viewFiles, manageFiles]
|
||||||
|
|
||||||
|
report:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||||
|
|
||||||
|
reportTemplate:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share, execute]
|
||||||
|
|
||||||
|
mosaicoTemplate:
|
||||||
|
master:
|
||||||
|
name: Master
|
||||||
|
description: All permissions
|
||||||
|
permissions: [view, edit, delete, share, viewFiles, manageFiles]
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const urllib = require('url');
|
const urllib = require('url');
|
||||||
|
const {anonymousRestrictedAccessToken} = require('../shared/urls');
|
||||||
|
|
||||||
function getTrustedUrlBase() {
|
function getTrustedUrlBase() {
|
||||||
return urllib.resolve(config.www.trustedUrlBase, '');
|
return urllib.resolve(config.www.trustedUrlBase, '');
|
||||||
|
@ -19,7 +20,7 @@ function getSandboxUrl(path, context) {
|
||||||
if (context && context.user && context.user.restrictedAccessToken) {
|
if (context && context.user && context.user.restrictedAccessToken) {
|
||||||
return urllib.resolve(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''));
|
return urllib.resolve(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''));
|
||||||
} else {
|
} else {
|
||||||
return urllib.resolve(config.www.sandboxUrlBase, 'ANONYMOUS/' + (path || ''));
|
return urllib.resolve(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,19 @@ async function listWithContentDTAjax(context, params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listOthersByListDTAjax(context, campaignId, listId, params) {
|
||||||
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
|
context,
|
||||||
|
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||||
|
params,
|
||||||
|
builder => builder.from('campaigns')
|
||||||
|
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||||
|
.whereNot('campaigns.id', campaignId)
|
||||||
|
.where('campaigns.list', listId),
|
||||||
|
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function getByIdTx(tx, context, id, withPermissions = true, content = Content.ALL) {
|
async function getByIdTx(tx, context, id, withPermissions = true, content = Content.ALL) {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
|
||||||
let entity = await tx('campaigns').where('id', id).first();
|
let entity = await tx('campaigns').where('id', id).first();
|
||||||
|
@ -309,8 +322,8 @@ async function enforceSendPermissionTx(tx, context, campaignId) {
|
||||||
|
|
||||||
const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
|
const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
|
||||||
|
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'send_configuration', campaign.send_configuration, requiredPermission);
|
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', campaign.send_configuration, requiredPermission);
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, requiredPermission);
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'send');
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is to handle circular dependency with triggers.js
|
// This is to handle circular dependency with triggers.js
|
||||||
|
@ -319,6 +332,7 @@ Object.assign(module.exports, {
|
||||||
hash,
|
hash,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
listWithContentDTAjax,
|
listWithContentDTAjax,
|
||||||
|
listOthersByListDTAjax,
|
||||||
getByIdTx,
|
getByIdTx,
|
||||||
getById,
|
getById,
|
||||||
create,
|
create,
|
||||||
|
|
|
@ -10,6 +10,7 @@ const shares = require('./shares');
|
||||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||||
const fields = require('./fields');
|
const fields = require('./fields');
|
||||||
const segments = require('./segments');
|
const segments = require('./segments');
|
||||||
|
const entitySettings = require('../lib/entity-settings');
|
||||||
|
|
||||||
const UnsubscriptionMode = require('../shared/lists').UnsubscriptionMode;
|
const UnsubscriptionMode = require('../shared/lists').UnsubscriptionMode;
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ function hash(entity) {
|
||||||
|
|
||||||
|
|
||||||
async function listDTAjax(context, params) {
|
async function listDTAjax(context, params) {
|
||||||
|
const campaignEntityType = entitySettings.getEntityType('campaign');
|
||||||
|
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
context,
|
context,
|
||||||
[{ entityTypeId: 'list', requiredOperations: ['view'] }],
|
[{ entityTypeId: 'list', requiredOperations: ['view'] }],
|
||||||
|
@ -28,7 +31,16 @@ async function listDTAjax(context, params) {
|
||||||
builder => builder
|
builder => builder
|
||||||
.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')
|
||||||
|
.whereRaw('campaigns.list = lists.id')
|
||||||
|
.innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`)
|
||||||
|
.where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers')
|
||||||
|
.innerJoin('triggers', 'campaigns.id', 'triggers.campaign')
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,10 @@ const { enforce, filterObject } = require('../lib/helpers');
|
||||||
const dtHelpers = require('../lib/dt-helpers');
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
const interoperableErrors = require('../shared/interoperable-errors');
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
const shares = require('./shares');
|
const shares = require('./shares');
|
||||||
const {EntityVals, ActionVals} = require('../shared/triggers');
|
const {EntityVals, EventVals, Entity} = require('../shared/triggers');
|
||||||
const campaigns = require('./campaigns');
|
const campaigns = require('./campaigns');
|
||||||
|
|
||||||
const allowedKeys = new Set(['name', 'description', 'entity', 'action', 'seconds_after', 'enabled', 'source_campaign']);
|
const allowedKeys = new Set(['name', 'description', 'entity', 'event', 'seconds_after', 'enabled', 'source_campaign']);
|
||||||
|
|
||||||
function hash(entity) {
|
function hash(entity) {
|
||||||
return hasher.hash(filterObject(entity, allowedKeys));
|
return hasher.hash(filterObject(entity, allowedKeys));
|
||||||
|
@ -34,9 +34,10 @@ async function listByCampaignDTAjax(context, campaignId, params) {
|
||||||
params,
|
params,
|
||||||
builder => builder
|
builder => builder
|
||||||
.from('triggers')
|
.from('triggers')
|
||||||
.innerJoin('campaigns', 'campaign.id', 'triggers.campaign')
|
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
|
||||||
|
.innerJoin('lists', 'lists.id', 'campaigns.list')
|
||||||
.where('triggers.campaign', campaignId),
|
.where('triggers.campaign', campaignId),
|
||||||
[ 'triggers.id', 'triggers.name', 'campaign.list', 'triggers.entity', 'triggers.action', 'triggers.source_campaign', 'triggers.enabled' ]
|
[ 'triggers.id', 'triggers.name', 'triggers.description', 'lists.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled' ]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -44,13 +45,13 @@ async function listByCampaignDTAjax(context, campaignId, params) {
|
||||||
async function listByListDTAjax(context, listId, params) {
|
async function listByListDTAjax(context, listId, params) {
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
context,
|
context,
|
||||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
[{ entityTypeId: 'campaign', requiredOperations: ['viewTriggers'] }],
|
||||||
params,
|
params,
|
||||||
builder => builder
|
builder => builder
|
||||||
.from('triggers')
|
.from('triggers')
|
||||||
.innerJoin('campaigns', 'campaign.id', 'triggers.campaign')
|
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
|
||||||
.where('campaign.list', listId),
|
.where('campaigns.list', listId),
|
||||||
[ 'triggers.id', 'triggers.name', 'trigger.campaign', 'triggers.entity', 'triggers.action', 'triggers.source_campaign', 'triggers.enabled' ]
|
[ 'triggers.id', 'triggers.name', 'triggers.description', 'campaigns.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled', 'triggers.campaign' ]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ async function _validateAndPreprocess(tx, context, campaignId, entity) {
|
||||||
enforce(Number.isInteger(entity.seconds_after));
|
enforce(Number.isInteger(entity.seconds_after));
|
||||||
enforce(entity.seconds_after >= 0, 'Seconds after must not be negative');
|
enforce(entity.seconds_after >= 0, 'Seconds after must not be negative');
|
||||||
enforce(entity.entity in EntityVals, 'Invalid entity');
|
enforce(entity.entity in EntityVals, 'Invalid entity');
|
||||||
enforce(entity.action in ActionVals[entity.entity], 'Invalid action');
|
enforce(entity.event in EventVals[entity.entity], 'Invalid event');
|
||||||
|
|
||||||
if (entity.entity === Entity.CAMPAIGN) {
|
if (entity.entity === Entity.CAMPAIGN) {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.source_campaign, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.source_campaign, 'view');
|
||||||
|
@ -94,7 +95,7 @@ async function updateWithConsistencyCheck(context, campaignId, entity) {
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'manageTriggers');
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'manageTriggers');
|
||||||
|
|
||||||
const existing = await tx('campaign').where({campaign: campaignId, id: entity.id}).first();
|
const existing = await tx('triggers').where({campaign: campaignId, id: entity.id}).first();
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
|
@ -138,7 +139,8 @@ Object.assign(module.exports, {
|
||||||
hash,
|
hash,
|
||||||
getById,
|
getById,
|
||||||
listByCampaignDTAjax,
|
listByCampaignDTAjax,
|
||||||
listByListDTAjax, create,
|
listByListDTAjax,
|
||||||
|
create,
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
removeTx,
|
removeTx,
|
||||||
remove,
|
remove,
|
||||||
|
|
|
@ -14,6 +14,11 @@ router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req,
|
||||||
return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
|
return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.postAsync('/campaigns-others-by-list-table/:campaignId/:listId', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await campaigns.listOthersByListDTAjax(req.context, req.params.campaignId, req.params.listId, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
router.getAsync('/campaigns-settings/:campaignId', passport.loggedIn, async (req, res) => {
|
router.getAsync('/campaigns-settings/:campaignId', passport.loggedIn, async (req, res) => {
|
||||||
const campaign = await campaigns.getById(req.context, req.params.campaignId, true, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
const campaign = await campaigns.getById(req.context, req.params.campaignId, true, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
||||||
campaign.hash = campaigns.hash(campaign, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
campaign.hash = campaigns.hash(campaign, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
||||||
|
|
|
@ -7,7 +7,7 @@ const entityTypesAddNamespace = ['list', 'custom_form', 'template', 'campaign',
|
||||||
const shareableEntityTypes = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'namespace', 'send_configuration', 'mosaico_template'];
|
const shareableEntityTypes = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'namespace', 'send_configuration', 'mosaico_template'];
|
||||||
const { MailerType, getSystemSendConfigurationId } = require('../../../shared/send-configurations');
|
const { MailerType, getSystemSendConfigurationId } = require('../../../shared/send-configurations');
|
||||||
const { enforce } = require('../../../lib/helpers');
|
const { enforce } = require('../../../lib/helpers');
|
||||||
const { EntityVals: TriggerEntityVals, ActionVals: TriggerActionVals } = require('../../../shared/triggers');
|
const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require('../../../shared/triggers');
|
||||||
|
|
||||||
const entityTypesWithFiles = {
|
const entityTypesWithFiles = {
|
||||||
campaign: {
|
campaign: {
|
||||||
|
@ -951,7 +951,7 @@ async function migrateAttachments(knex) {
|
||||||
async function migrateTriggers(knex) {
|
async function migrateTriggers(knex) {
|
||||||
await knex.schema.table('triggers', table => {
|
await knex.schema.table('triggers', table => {
|
||||||
table.renameColumn('rule', 'entity');
|
table.renameColumn('rule', 'entity');
|
||||||
table.renameColumn('column', 'action');
|
table.renameColumn('column', 'event');
|
||||||
table.renameColumn('dest_campaign', 'campaign');
|
table.renameColumn('dest_campaign', 'campaign');
|
||||||
table.renameColumn('seconds', 'seconds_after');
|
table.renameColumn('seconds', 'seconds_after');
|
||||||
});
|
});
|
||||||
|
@ -964,7 +964,7 @@ async function migrateTriggers(knex) {
|
||||||
enforce(campaign.list === trigger.list, 'The list of trigger and campaign have to be the same.');
|
enforce(campaign.list === trigger.list, 'The list of trigger and campaign have to be the same.');
|
||||||
|
|
||||||
enforce(trigger.entity in TriggerEntityVals);
|
enforce(trigger.entity in TriggerEntityVals);
|
||||||
enforce(trigger.action in TriggerActionVals[trigger.entity]);
|
enforce(trigger.event in TriggerEventVals[trigger.entity]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await knex.schema.table('triggers', table => {
|
await knex.schema.table('triggers', table => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ const Entity = {
|
||||||
CAMPAIGN: 'campaign'
|
CAMPAIGN: 'campaign'
|
||||||
};
|
};
|
||||||
|
|
||||||
const Action = {
|
const Event = {
|
||||||
[Entity.SUBSCRIPTION]: {
|
[Entity.SUBSCRIPTION]: {
|
||||||
CREATED: 'created',
|
CREATED: 'created',
|
||||||
LATEST_OPEN: 'latest_open',
|
LATEST_OPEN: 'latest_open',
|
||||||
|
@ -25,7 +25,7 @@ const EntityVals = {
|
||||||
campaign: 'CAMPAIGN'
|
campaign: 'CAMPAIGN'
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActionVals = {
|
const EventVals = {
|
||||||
[Entity.SUBSCRIPTION]: {
|
[Entity.SUBSCRIPTION]: {
|
||||||
created: 'CREATED',
|
created: 'CREATED',
|
||||||
latest_open: 'LATEST_OPEN',
|
latest_open: 'LATEST_OPEN',
|
||||||
|
@ -42,7 +42,7 @@ const ActionVals = {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Entity,
|
Entity,
|
||||||
Action,
|
Event,
|
||||||
EntityVals,
|
EntityVals,
|
||||||
ActionVals
|
EventVals
|
||||||
};
|
};
|
7
shared/urls.js
Normal file
7
shared/urls.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const anonymousRestrictedAccessToken = 'public';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
anonymousRestrictedAccessToken
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue