Before renaming imports to tasks

This commit is contained in:
Tomas Bures 2018-09-23 22:28:58 +02:00
parent a494dc6482
commit 86efa11994
18 changed files with 81 additions and 58 deletions

12
TODO.md
View file

@ -1,3 +1,6 @@
### Front page
- Some dashboard
### Deletion
- Delete button in Lists
- Check/delete dependencies
@ -19,4 +22,13 @@
- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
### API
- Add API extensions
### GDPR
- Removal of personal data upon unsubscribe (settable per list)
- Refuse editing subscriptions which have been anonymized
- Add field to subscriptions which says till when the consent has been given
- Provide a link (and merge tag) that will update the consent date to now
- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds)
- Removal of personal data upon consent expiration (settable per list)

View file

@ -13,4 +13,4 @@ The migration should happen almost automatically. There are however the followin
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be
part of the URL - e.g. webhooks/zone-mta/sender-config/default.
part of the URL - e.g. webhooks/zone-mta/sender-config/system.

View file

@ -141,7 +141,7 @@ function createApp(appType) {
app.disable('x-powered-by');
app.use(compression());
app.use(favicon(path.join(__dirname, 'client', 'public', 'favicon.ico')));
app.use(favicon(path.join(__dirname, 'client', 'static', 'favicon.ico')));
app.use(logger(config.www.log, {
stream: {
@ -182,7 +182,7 @@ function createApp(appType) {
app.use(passport.tryAuthByRestrictedAccessToken);
}
useWith404Fallback('/public', express.static(path.join(__dirname, 'client', 'public')));
useWith404Fallback('/static', express.static(path.join(__dirname, 'client', 'static')));
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist')));
useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales')));

View file

@ -559,10 +559,11 @@ export default class CUD extends Component {
const sendConfigurationsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') }
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
];
let sendSettings;

View file

@ -84,7 +84,7 @@ export default class CUD extends Component {
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
data.daysAfter = (Math.round(data.seconds_after / (3600 * 24))).toString();
data.daysAfter = (Math.round(data.seconds / (3600 * 24))).toString();
if (data.entity === Entity.SUBSCRIPTION) {
data.subscriptionEvent = data.event;
@ -157,7 +157,7 @@ export default class CUD extends Component {
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.seconds_after = Number.parseInt(data.daysAfter) * 3600 * 24;
data.seconds = Number.parseInt(data.daysAfter) * 3600 * 24;
if (data.entity === Entity.SUBSCRIPTION) {
data.event = data.subscriptionEvent;

View file

@ -156,9 +156,10 @@ export default class CUD extends Component {
const sendConfigurationsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('Namespace') }
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 6, title: t('Namespace') }
];
return (

View file

@ -96,7 +96,7 @@ export default class List extends Component {
if (perms.includes('viewImports')) {
actions.push({
label: <Icon icon="sort" title={t('Imports')}/>,
label: <Icon icon="sort" title={t('Imports & Tasks')}/>,
link: `/lists/${data[0]}/imports`
});
}

View file

@ -386,7 +386,7 @@ export default class CUD extends Component {
let saveButtonLabel;
if (!isEdit) {
saveButtonLabel = t('Save and edit mapping');
saveButtonLabel = t('Save and edit settings');
} else {
saveButtonLabel = t('Save');
}
@ -404,7 +404,7 @@ export default class CUD extends Component {
deletedMsg={t('Field deleted')}/>
}
<Title>{isEdit ? t('Edit Import') : t('Create Import')}</Title>
<Title>{isEdit ? t('Edit Import/Task') : t('Create Import/Task')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>

View file

@ -81,11 +81,11 @@ export default class List extends Component {
<div>
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') &&
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import/Task')}/>
</Toolbar>
}
<Title>{t('Imports')}</Title>
<Title>{t('Imports & Tasks')}</Title>
<Table withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} />
</div>

View file

@ -132,7 +132,7 @@ function getMenus(t) {
}
},
imports: {
title: t('Imports'),
title: t('Imports & Tasks'),
link: params => `/lists/${params.listId}/imports/`,
visible: resolved => resolved.list.permissions.includes('viewImports'),
panelRender: props => <ImportsList list={props.resolved.list} />,

View file

@ -18,6 +18,7 @@ const executor = require('./lib/executor');
const privilegeHelpers = require('./lib/privilege-helpers');
const knex = require('./lib/knex');
const shares = require('./models/shares');
const { AppType } = require('./shared/app');
const trustedPort = config.www.trustedPort;
const sandboxPort = config.www.sandboxPort;
@ -31,7 +32,7 @@ if (config.title) {
log.level = config.log.level;
function startHTTPServer(appType, port, callback) {
function startHTTPServer(appType, appName, port, callback) {
const app = appBuilder.createApp(appType);
app.set('port', port);
@ -60,7 +61,7 @@ function startHTTPServer(appType, port, callback) {
server.on('listening', () => {
const addr = server.address();
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
log.info('Express', 'WWW server listening on %s', bind);
log.info('Express', 'WWW server [%s] listening on %s', appName, bind);
});
server.listen({port, host}, callback);
@ -85,9 +86,9 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
executor.spawn(() => {
testServer(() => {
verpServer(() => {
startHTTPServer(AppType.TRUSTED, trustedPort, () => {
startHTTPServer(AppType.SANDBOXED, sandboxPort, () => {
startHTTPServer(AppType.PUBLIC, publicPort, () => {
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () => {
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () => {
startHTTPServer(AppType.PUBLIC, 'public', publicPort, () => {
privilegeHelpers.dropRootPrivileges();
tzupdate.start();

View file

@ -172,6 +172,20 @@ async function listTestUsersDTAjax(context, campaignId, params) {
});
}
async function getTrackingSettingsByCidTx(tx, cid) {
const entity = await tx('campaigns').where('campaigns.cid', cid)
.select([
'campaigns.id', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled'
])
.first();
if (!entity) {
throw new interoperableErrors.NotFoundError();
}
return entity;
}
async function rawGetByTx(tx, key, id) {
const entity = await tx('campaigns').where('campaigns.' + key, id)
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
@ -709,8 +723,6 @@ module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIn
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;
module.exports.getByCidTx = getByCidTx;
module.exports.getByCid = getByCid;
module.exports.create = create;
module.exports.createRssTx = createRssTx;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
@ -731,4 +743,5 @@ module.exports.start = start;
module.exports.stop = stop;
module.exports.reset = reset;
module.exports.rawGetBy = rawGetBy;
module.exports.rawGetByTx = rawGetByTx;
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;

View file

@ -4,7 +4,6 @@ const log = require('npmlog');
const knex = require('../lib/knex');
const dtHelpers = require('../lib/dt-helpers');
const shares = require('./shares');
const tools = require('../lib/tools');
const campaigns = require('./campaigns');
const lists = require('./lists');
const subscriptions = require('./subscriptions');
@ -28,7 +27,7 @@ async function resolve(linkCid) {
async function countLink(remoteIp, userAgent, campaignCid, listCid, subscriptionCid, linkId) {
await knex.transaction(async tx => {
const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), listCid);
const campaign = await campaigns.getByCidTx(tx, contextHelpers.getAdminContext(), campaignCid);
const campaign = await campaigns.getTrackingSettingsByCidTx(tx, campaignCid);
const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), subscriptionCid);
const country = geoip.lookupCountry(remoteIp) || null;

View file

@ -9,7 +9,7 @@ const shares = require('./shares');
const {EntityVals, EventVals, Entity} = require('../shared/triggers');
const campaigns = require('./campaigns');
const allowedKeys = new Set(['name', 'description', 'entity', 'event', 'seconds_after', 'enabled', 'source_campaign']);
const allowedKeys = new Set(['name', 'description', 'entity', 'event', 'seconds', 'enabled', 'source_campaign']);
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
@ -36,7 +36,7 @@ async function listByCampaignDTAjax(context, campaignId, params) {
.from('triggers')
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
.where('triggers.campaign', campaignId),
[ 'triggers.id', 'triggers.name', 'triggers.description', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled' ]
[ 'triggers.id', 'triggers.name', 'triggers.description', 'triggers.entity', 'triggers.event', 'triggers.seconds', 'triggers.enabled' ]
);
});
}
@ -51,13 +51,13 @@ async function listByListDTAjax(context, listId, params) {
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
.innerJoin('campaign_lists', 'campaign_lists.campaign', 'campaigns.id')
.where('campaign_lists.list', listId),
[ 'triggers.id', 'triggers.name', 'triggers.description', 'campaigns.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled', 'triggers.campaign' ]
[ 'triggers.id', 'triggers.name', 'triggers.description', 'campaigns.name', 'triggers.entity', 'triggers.event', 'triggers.seconds', 'triggers.enabled', 'triggers.campaign' ]
);
}
async function _validateAndPreprocess(tx, context, campaignId, entity) {
enforce(Number.isInteger(entity.seconds_after));
enforce(entity.seconds_after >= 0, 'Seconds after must not be negative');
enforce(Number.isInteger(entity.seconds));
enforce(entity.seconds >= 0, 'Seconds must not be negative');
enforce(entity.entity in EntityVals, 'Invalid entity');
enforce(entity.event in EventVals[entity.entity], 'Invalid event');

View file

@ -154,25 +154,23 @@ router.postAsync('/mailgun', uploads.any(), async (req, res) => {
const evt = req.body;
const message = await campaigns.getMessageByCid([].concat(evt && evt.campaign_id || []).shift());
if (!message) {
continue;
}
if (message) {
switch (evt.event) {
case 'bounced':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
break;
switch (evt.event) {
case 'bounced':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
break;
case 'complained':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'complained':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'unsubscribed':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
case 'unsubscribed':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
}
}
return res.json({
@ -188,12 +186,11 @@ router.postAsync('/zone-mta', async (req, res) => {
if (req.body.id) {
const message = await campaigns.getMessageByCid(req.body.id);
if (!message) {
continue;
}
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
if (message) {
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
}
}
res.json({

View file

@ -131,7 +131,7 @@ async function start() {
}
sqlQry = sqlQry.where(column, '<=', currentTs - trigger.seconds_after);
sqlQry = sqlQry.where(column, '<=', currentTs - trigger.seconds);
if (trigger.last_check !== null) {
sqlQry = sqlQry.where(column, '>', trigger.last_check);

View file

@ -1065,7 +1065,6 @@ async function migrateTriggers(knex) {
table.renameColumn('rule', 'entity');
table.renameColumn('column', 'event');
table.renameColumn('dest_campaign', 'campaign');
table.renameColumn('seconds', 'seconds_after');
});
const triggers = await knex('triggers');

View file

@ -11,7 +11,7 @@ function getSystemSendConfigurationId() {
}
function getSystemSendConfigurationCid() {
return 'default';
return 'system';
}
module.exports = {